fix: restore trade reflection system code that was accidentally deleted

refactor3.0
Lxy 1 week ago
parent f663435843
commit 7168d8f3e9

@ -7,6 +7,9 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from datetime import datetime
from app.mysql_database import MySQLSessionLocal
from app.storage_manager import get_storage_manager
logger = logging.getLogger(__name__)
# 数据目录
@ -28,14 +31,38 @@ AnalysisBase = declarative_base()
def get_analysis_db():
"""获取期货智析数据库会话。
分析数据量较小复盘计划评分等始终使用 SQLite 以保证稳定性
AnalysisBase 绑定的是 analysis_engine (SQLite)ORM 模型均基于此定义
根据 MySQL 可用性动态选择后端
- MySQL 可用时返回 MySQL Session
- MySQL 不可用时返回 SQLite Session兜底
"""
db = AnalysisSessionLocal()
try:
yield db
finally:
db.close()
storage = get_storage_manager()
if MySQLSessionLocal is not None and storage.check_mysql():
db = None
try:
db = MySQLSessionLocal()
yield db
except Exception as e:
logger.warning(f"创建 MySQL analysis session 失败,降级到 SQLite: {e}")
if db is not None:
try:
db.close()
except Exception:
pass
db = AnalysisSessionLocal()
yield db
finally:
if db is not None:
try:
db.close()
except Exception:
pass
else:
db = AnalysisSessionLocal()
try:
yield db
finally:
db.close()
def init_analysis_db():
@ -48,12 +75,17 @@ def init_analysis_db():
AIAnalysisCache, ReviewDate, SymbolRanking, TradingPlan,
SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2,
TradeRecord, TradeImportBatch,
TradePair, TradeReflection, DailyReflection, TradeTag, TradeRecordTag, TradeExperience,
TradeAIAnalysis,
)
AnalysisBase.metadata.create_all(bind=analysis_engine)
# 迁移为已有表添加新增列SQLite 不支持 IF NOT EXISTS用异常处理
_migrate_add_columns()
# 初始化预设标签
_init_preset_tags()
def init_analysis_mysql(mysql_engine):
"""初始化期货智析数据库表MySQL"""
@ -63,6 +95,8 @@ def init_analysis_mysql(mysql_engine):
AIAnalysisCache, ReviewDate, SymbolRanking, TradingPlan,
SymbolScoreV2, TradingPlanV2, SectorHeat, ReviewPlanV2,
TradeRecord, TradeImportBatch,
TradePair, TradeReflection, DailyReflection, TradeTag, TradeRecordTag, TradeExperience,
TradeAIAnalysis,
)
AnalysisBase.metadata.create_all(bind=mysql_engine)
logger.info("MySQL analysis 表结构初始化完成")
@ -88,3 +122,53 @@ def _migrate_add_columns():
conn.commit()
conn.close()
def _init_preset_tags():
"""初始化预设标签"""
from app.analysis_models import TradeTag
from sqlalchemy import inspect
session = AnalysisSessionLocal()
try:
# 检查表是否存在
inspector = inspect(session.get_bind())
if 'trade_tags' not in inspector.get_table_names():
return
# 检查是否已有预设标签
existing = session.query(TradeTag).filter_by(is_preset=True).first()
if existing:
return
preset_tags = [
# 操作类
TradeTag(name="追涨杀跌", category="operation", is_preset=True),
TradeTag(name="逆市操作", category="operation", is_preset=True),
TradeTag(name="提前入场", category="operation", is_preset=True),
TradeTag(name="延迟出场", category="operation", is_preset=True),
TradeTag(name="加仓过早", category="operation", is_preset=True),
TradeTag(name="减仓过晚", category="operation", is_preset=True),
# 心态类
TradeTag(name="情绪化交易", category="mindset", is_preset=True),
TradeTag(name="恐惧平仓", category="mindset", is_preset=True),
TradeTag(name="贪婪持仓", category="mindset", is_preset=True),
TradeTag(name="侥幸心理", category="mindset", is_preset=True),
# 纪律类
TradeTag(name="严格执行计划", category="discipline", is_preset=True),
TradeTag(name="违反止损", category="discipline", is_preset=True),
TradeTag(name="超仓交易", category="discipline", is_preset=True),
TradeTag(name="频繁交易", category="discipline", is_preset=True),
# 技术类
TradeTag(name="突破交易", category="technical", is_preset=True),
TradeTag(name="回调交易", category="technical", is_preset=True),
TradeTag(name="趋势跟踪", category="technical", is_preset=True),
TradeTag(name="震荡交易", category="technical", is_preset=True),
TradeTag(name="反转交易", category="technical", is_preset=True),
]
session.add_all(preset_tags)
session.commit()
except Exception:
session.rollback()
finally:
session.close()

@ -323,3 +323,155 @@ class TradeImportBatch(AnalysisBase):
def __repr__(self):
return f"<TradeImportBatch {self.batch_id} {self.source_file}>"
# ==================== 交易反思系统模型 ====================
class TradePair(AnalysisBase):
"""交易配对表 - 将开仓和平仓配对为一个完整交易"""
__tablename__ = "trade_pairs"
id = Column(Integer, primary_key=True, autoincrement=True)
variety = Column(String(16), nullable=False, index=True, comment="品种代码 AG")
direction = Column(String(8), nullable=False, comment="方向: long/short")
open_record_ids = Column(JSON, nullable=False, comment="开仓记录ID列表 [id1, id2, ...]")
close_record_ids = Column(JSON, nullable=False, comment="平仓记录ID列表 [id1, id2, ...]")
open_price = Column(Float, nullable=True, comment="平均开仓价")
close_price = Column(Float, nullable=True, comment="平均平仓价")
total_volume = Column(Float, nullable=True, comment="总手数")
close_pnl = Column(Float, nullable=True, default=0.0, comment="平仓盈亏")
total_commission = Column(Float, nullable=True, default=0.0, comment="总手续费")
net_pnl = Column(Float, nullable=True, default=0.0, comment="净盈亏 = 平仓盈亏 - 手续费")
open_date = Column(String(16), nullable=True, index=True, comment="开仓日期")
close_date = Column(String(16), nullable=True, index=True, comment="平仓日期")
created_at = Column(DateTime, nullable=False, default=datetime.now)
__table_args__ = (
Index('ix_trade_pairs_variety_date', 'variety', 'open_date'),
)
def __repr__(self):
return f"<TradePair {self.variety} {self.direction} pnl={self.net_pnl}>"
class TradeReflection(AnalysisBase):
"""交易反思表 - 每笔交易的反思内容"""
__tablename__ = "trade_reflections"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_pair_id = Column(Integer, nullable=False, index=True, comment="关联交易配对ID")
trade_date = Column(String(16), nullable=False, index=True, comment="交易日期")
# 模板字段
entry_reason = Column(Text, nullable=True, comment="入场理由")
entry_timing = Column(String(8), nullable=True, comment="入场时机评价: good/fair/poor")
position_management = Column(Text, nullable=True, comment="仓位管理反思")
exit_reason = Column(Text, nullable=True, comment="出场理由")
exit_timing = Column(String(8), nullable=True, comment="出场时机评价: good/fair/poor")
discipline_score = Column(Integer, nullable=True, comment="纪律评分 1-5")
# 自由反思
free_reflection = Column(Text, nullable=True, comment="自由反思内容")
# 状态
ai_analyzed = Column(Boolean, nullable=False, default=False, comment="是否已AI分析")
ai_version = Column(Integer, nullable=True, comment="AI分析版本号")
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<TradeReflection pair_id={self.trade_pair_id} date={self.trade_date}>"
class DailyReflection(AnalysisBase):
"""当日反思表 - 每天的整体反思"""
__tablename__ = "daily_reflections"
id = Column(Integer, primary_key=True, autoincrement=True)
reflection_date = Column(String(16), nullable=False, unique=True, index=True, comment="反思日期 YYYY-MM-DD")
emotion_state = Column(String(16), nullable=True, comment="情绪状态: 平静/兴奋/焦虑/恐惧/贪婪")
market_judgment = Column(Text, nullable=True, comment="市场判断")
discipline_score = Column(Integer, nullable=True, comment="执行纪律评分 1-5")
overall_rating = Column(Integer, nullable=True, comment="总体评价 1-5")
summary = Column(Text, nullable=True, comment="总结")
improvements = Column(Text, nullable=True, comment="改进方向")
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
def __repr__(self):
return f"<DailyReflection {self.reflection_date}>"
class TradeTag(AnalysisBase):
"""标签定义表"""
__tablename__ = "trade_tags"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True, index=True, comment="标签名称")
category = Column(String(16), nullable=False, comment="分类: operation/mindset/discipline/technical/custom")
is_preset = Column(Boolean, nullable=False, default=False, comment="是否预设标签")
color = Column(String(16), nullable=True, comment="标签颜色(前端用)")
created_at = Column(DateTime, nullable=False, default=datetime.now)
def __repr__(self):
return f"<TradeTag {self.name} [{self.category}]>"
class TradeRecordTag(AnalysisBase):
"""交易-标签关联表"""
__tablename__ = "trade_record_tags"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_pair_id = Column(Integer, nullable=False, index=True, comment="交易配对ID")
tag_id = Column(Integer, nullable=False, index=True, comment="标签ID")
created_at = Column(DateTime, nullable=False, default=datetime.now)
__table_args__ = (
UniqueConstraint("trade_pair_id", "tag_id", name="uq_pair_tag"),
)
def __repr__(self):
return f"<TradeRecordTag pair_id={self.trade_pair_id} tag_id={self.tag_id}>"
class TradeExperience(AnalysisBase):
"""经验教训表"""
__tablename__ = "trade_experiences"
id = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(128), nullable=False, comment="经验标题")
content = Column(Text, nullable=False, comment="经验内容")
exp_type = Column(String(16), nullable=False, comment="类型: lesson/tip/warning")
source_pair_id = Column(Integer, nullable=True, index=True, comment="来源交易配对ID")
source_date = Column(String(16), nullable=True, index=True, comment="来源交易日期")
tags = Column(JSON, nullable=True, comment="关联标签名称列表")
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
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}>"

@ -18,6 +18,18 @@ from app.services.trade_parser import (
calc_overall_statistics,
get_trade_pairs,
)
from app.services.trade_pairing_engine import (
auto_pair_trades,
get_daily_trades_with_pairs,
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=["交易复盘"])
@ -588,3 +600,538 @@ async def analyze_trade(
except Exception as e:
logger.exception("AI分析交易失败")
return {"success": False, "message": f"AI分析失败: {str(e)}"}
# ==================== 交易反思系统 API ====================
from app.analysis_models import (
TradePair, TradeReflection, DailyReflection,
TradeTag, TradeRecordTag, TradeExperience,
)
from pydantic import BaseModel
# --- 按天交易视图 ---
@router.get("/daily-trades/{trade_date}")
def api_get_daily_trades(
trade_date: str,
db: Session = Depends(get_analysis_db),
):
"""获取指定日期的交易+配对+反思状态"""
# 先执行自动配对
auto_pair_trades(db, trade_date)
db.commit()
data = get_daily_trades_with_pairs(db, trade_date)
return {"success": True, "data": data}
# --- 手动配对 ---
class ManualPairRequest(BaseModel):
open_record_ids: list[int]
close_record_ids: list[int]
@router.post("/trade-pairs")
def api_create_manual_pair(
req: ManualPairRequest,
db: Session = Depends(get_analysis_db),
):
"""手动创建交易配对"""
try:
pair = create_manual_pair(db, req.open_record_ids, req.close_record_ids)
db.commit()
return {"success": True, "data": {"id": pair.id}}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
db.rollback()
logger.exception("创建配对失败")
raise HTTPException(status_code=500, detail=f"创建配对失败: {str(e)}")
@router.delete("/trade-pairs/{pair_id}")
def api_delete_pair(
pair_id: int,
db: Session = Depends(get_analysis_db),
):
"""删除交易配对"""
success = delete_pair(db, pair_id)
if not success:
raise HTTPException(status_code=404, detail="配对不存在")
db.commit()
return {"success": True, "message": "配对已删除"}
# --- 反思 CRUD ---
class ReflectionRequest(BaseModel):
trade_pair_id: int
trade_date: str
entry_reason: Optional[str] = None
entry_timing: Optional[str] = None
position_management: Optional[str] = None
exit_reason: Optional[str] = None
exit_timing: Optional[str] = None
discipline_score: Optional[int] = None
free_reflection: Optional[str] = None
@router.post("/reflections")
def api_create_reflection(
req: ReflectionRequest,
db: Session = Depends(get_analysis_db),
):
"""创建交易反思"""
# 检查是否已存在
existing = db.query(TradeReflection).filter(
TradeReflection.trade_pair_id == req.trade_pair_id
).first()
if existing:
raise HTTPException(status_code=400, detail="该交易已有反思,请使用更新接口")
reflection = TradeReflection(
trade_pair_id=req.trade_pair_id,
trade_date=req.trade_date,
entry_reason=req.entry_reason,
entry_timing=req.entry_timing,
position_management=req.position_management,
exit_reason=req.exit_reason,
exit_timing=req.exit_timing,
discipline_score=req.discipline_score,
free_reflection=req.free_reflection,
)
db.add(reflection)
db.commit()
return {"success": True, "data": {"id": reflection.id}}
@router.put("/reflections/{reflection_id}")
def api_update_reflection(
reflection_id: int,
req: ReflectionRequest,
db: Session = Depends(get_analysis_db),
):
"""更新交易反思"""
reflection = db.query(TradeReflection).filter(TradeReflection.id == reflection_id).first()
if not reflection:
raise HTTPException(status_code=404, detail="反思不存在")
# 更新字段
for field in ['entry_reason', 'entry_timing', 'position_management',
'exit_reason', 'exit_timing', 'discipline_score', 'free_reflection']:
val = getattr(req, field)
if val is not None:
setattr(reflection, field, val)
# 重新保存时重置 AI 分析状态(因为内容变了)
reflection.ai_analyzed = False
reflection.ai_version = None
db.commit()
return {"success": True, "message": "反思已更新"}
@router.get("/reflections")
def api_get_reflections(
trade_pair_id: Optional[int] = Query(None),
trade_date: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""查询反思列表"""
query = db.query(TradeReflection)
if trade_pair_id:
query = query.filter(TradeReflection.trade_pair_id == trade_pair_id)
if trade_date:
query = query.filter(TradeReflection.trade_date == trade_date)
reflections = query.order_by(TradeReflection.created_at.desc()).all()
return {"success": True, "data": [{
"id": r.id,
"trade_pair_id": r.trade_pair_id,
"trade_date": r.trade_date,
"entry_reason": r.entry_reason,
"entry_timing": r.entry_timing,
"position_management": r.position_management,
"exit_reason": r.exit_reason,
"exit_timing": r.exit_timing,
"discipline_score": r.discipline_score,
"free_reflection": r.free_reflection,
"ai_analyzed": r.ai_analyzed,
"ai_version": r.ai_version,
"created_at": r.created_at.strftime('%Y-%m-%d %H:%M:%S') if r.created_at else None,
"updated_at": r.updated_at.strftime('%Y-%m-%d %H:%M:%S') if r.updated_at else None,
} for r in reflections]}
# --- 当日反思 CRUD ---
class DailyReflectionRequest(BaseModel):
reflection_date: str
emotion_state: Optional[str] = None
market_judgment: Optional[str] = None
discipline_score: Optional[int] = None
overall_rating: Optional[int] = None
summary: Optional[str] = None
improvements: Optional[str] = None
@router.post("/daily-reflections")
def api_create_daily_reflection(
req: DailyReflectionRequest,
db: Session = Depends(get_analysis_db),
):
"""创建或更新当日反思"""
existing = db.query(DailyReflection).filter(
DailyReflection.reflection_date == req.reflection_date
).first()
if existing:
# 更新
for field in ['emotion_state', 'market_judgment', 'discipline_score',
'overall_rating', 'summary', 'improvements']:
val = getattr(req, field)
if val is not None:
setattr(existing, field, val)
db.commit()
return {"success": True, "data": {"id": existing.id, "action": "updated"}}
else:
# 创建
dr = DailyReflection(
reflection_date=req.reflection_date,
emotion_state=req.emotion_state,
market_judgment=req.market_judgment,
discipline_score=req.discipline_score,
overall_rating=req.overall_rating,
summary=req.summary,
improvements=req.improvements,
)
db.add(dr)
db.commit()
return {"success": True, "data": {"id": dr.id, "action": "created"}}
@router.get("/daily-reflections/{reflection_date}")
def api_get_daily_reflection(
reflection_date: str,
db: Session = Depends(get_analysis_db),
):
"""获取当日反思"""
dr = db.query(DailyReflection).filter(
DailyReflection.reflection_date == reflection_date
).first()
if not dr:
return {"success": True, "data": None}
return {"success": True, "data": {
"id": dr.id,
"reflection_date": dr.reflection_date,
"emotion_state": dr.emotion_state,
"market_judgment": dr.market_judgment,
"discipline_score": dr.discipline_score,
"overall_rating": dr.overall_rating,
"summary": dr.summary,
"improvements": dr.improvements,
}}
# --- 标签系统 ---
class TagRequest(BaseModel):
name: str
category: str = "custom"
@router.get("/tags")
def api_get_tags(
category: Optional[str] = Query(None),
db: Session = Depends(get_analysis_db),
):
"""获取所有标签"""
query = db.query(TradeTag)
if category:
query = query.filter(TradeTag.category == category)
tags = query.order_by(TradeTag.is_preset.desc(), TradeTag.name).all()
return {"success": True, "data": [{
"id": t.id,
"name": t.name,
"category": t.category,
"is_preset": t.is_preset,
} for t in tags]}
@router.post("/tags")
def api_create_tag(
req: TagRequest,
db: Session = Depends(get_analysis_db),
):
"""创建自定义标签(含去重)"""
existing = db.query(TradeTag).filter(TradeTag.name == req.name).first()
if existing:
return {"success": True, "data": {"id": existing.id, "action": "exists"}}
tag = TradeTag(name=req.name, category=req.category, is_preset=False)
db.add(tag)
db.commit()
return {"success": True, "data": {"id": tag.id, "action": "created"}}
@router.post("/trade-pairs/{pair_id}/tags")
def api_add_tag_to_trade(
pair_id: int,
tag_id: int,
db: Session = Depends(get_analysis_db),
):
"""给交易打标签"""
# 检查是否已存在
existing = db.query(TradeRecordTag).filter(
TradeRecordTag.trade_pair_id == pair_id,
TradeRecordTag.tag_id == tag_id,
).first()
if existing:
return {"success": True, "message": "标签已存在"}
relation = TradeRecordTag(trade_pair_id=pair_id, tag_id=tag_id)
db.add(relation)
db.commit()
return {"success": True, "message": "标签已添加"}
@router.delete("/trade-pairs/{pair_id}/tags/{tag_id}")
def api_remove_tag_from_trade(
pair_id: int,
tag_id: int,
db: Session = Depends(get_analysis_db),
):
"""移除交易标签"""
db.query(TradeRecordTag).filter(
TradeRecordTag.trade_pair_id == pair_id,
TradeRecordTag.tag_id == tag_id,
).delete()
db.commit()
return {"success": True, "message": "标签已移除"}
@router.get("/trade-pairs/{pair_id}/tags")
def api_get_trade_tags(
pair_id: int,
db: Session = Depends(get_analysis_db),
):
"""获取交易的标签列表"""
relations = db.query(TradeRecordTag).filter(
TradeRecordTag.trade_pair_id == pair_id
).all()
tag_ids = [r.tag_id for r in relations]
tags = db.query(TradeTag).filter(TradeTag.id.in_(tag_ids)).all()
return {"success": True, "data": [{
"id": t.id,
"name": t.name,
"category": t.category,
} for t in tags]}
# --- 经验库 ---
class ExperienceRequest(BaseModel):
title: str
content: str
exp_type: str # lesson/tip/warning
source_pair_id: Optional[int] = None
source_date: Optional[str] = None
tags: Optional[list[str]] = None
@router.get("/experiences")
def api_get_experiences(
exp_type: Optional[str] = Query(None),
tag: Optional[str] = Query(None),
keyword: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_analysis_db),
):
"""获取经验列表(支持筛选和分页)"""
query = db.query(TradeExperience)
if exp_type:
query = query.filter(TradeExperience.exp_type == exp_type)
if tag:
query = query.filter(TradeExperience.tags.contains([tag]))
if keyword:
query = query.filter(
(TradeExperience.title.contains(keyword)) |
(TradeExperience.content.contains(keyword))
)
total = query.count()
experiences = query.order_by(
TradeExperience.created_at.desc()
).offset((page - 1) * page_size).limit(page_size).all()
return {"success": True, "data": {
"total": total,
"page": page,
"page_size": page_size,
"items": [{
"id": e.id,
"title": e.title,
"content": e.content,
"exp_type": e.exp_type,
"source_pair_id": e.source_pair_id,
"source_date": e.source_date,
"tags": e.tags or [],
"created_at": e.created_at.strftime('%Y-%m-%d %H:%M:%S') if e.created_at else None,
} for e in experiences],
}}
@router.post("/experiences")
def api_create_experience(
req: ExperienceRequest,
db: Session = Depends(get_analysis_db),
):
"""创建经验"""
exp = TradeExperience(
title=req.title,
content=req.content,
exp_type=req.exp_type,
source_pair_id=req.source_pair_id,
source_date=req.source_date,
tags=req.tags or [],
)
db.add(exp)
db.commit()
return {"success": True, "data": {"id": exp.id}}
@router.put("/experiences/{exp_id}")
def api_update_experience(
exp_id: int,
req: ExperienceRequest,
db: Session = Depends(get_analysis_db),
):
"""更新经验"""
exp = db.query(TradeExperience).filter(TradeExperience.id == exp_id).first()
if not exp:
raise HTTPException(status_code=404, detail="经验不存在")
for field in ['title', 'content', 'exp_type', 'source_pair_id', 'source_date', 'tags']:
val = getattr(req, field)
if val is not None:
setattr(exp, field, val)
db.commit()
return {"success": True, "message": "经验已更新"}
@router.delete("/experiences/{exp_id}")
def api_delete_experience(
exp_id: int,
db: Session = Depends(get_analysis_db),
):
"""删除经验"""
exp = db.query(TradeExperience).filter(TradeExperience.id == exp_id).first()
if not exp:
raise HTTPException(status_code=404, detail="经验不存在")
db.delete(exp)
db.commit()
return {"success": True, "message": "经验已删除"}
@router.get("/experiences/tag-stats")
def api_get_experience_tag_stats(
db: Session = Depends(get_analysis_db),
):
"""按标签统计经验数量"""
from collections import Counter
experiences = db.query(TradeExperience).all()
tag_counter = Counter()
for exp in experiences:
for tag in (exp.tags or []):
tag_counter[tag] += 1
# 同时返回所有标签的定义信息
tags = db.query(TradeTag).all()
tag_map = {t.name: {"id": t.id, "category": t.category, "is_preset": t.is_preset} for t in tags}
data = []
for name, count in tag_counter.most_common():
info = tag_map.get(name, {})
data.append({
"name": name,
"count": count,
"tag_id": info.get("id"),
"category": info.get("category", "custom"),
"is_preset": info.get("is_preset", False),
})
return {"success": True, "data": data}
# ==================== 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}}

@ -0,0 +1,336 @@
"""
交易配对引擎 - 将开仓和平仓配对为完整交易持久化到 TradePair
"""
import logging
from typing import Optional
from sqlalchemy.orm import Session
from collections import defaultdict
from app.analysis_models import TradeRecord, TradePair, TradeReflection, DailyReflection
logger = logging.getLogger(__name__)
def auto_pair_trades(db: Session, trade_date: str) -> dict:
"""
自动配对指定日期及之前未平仓的交易记录
按品种+方向分组FIFO 规则配对
返回{created: 新建配对数, updated: 更新配对数, unpaired: 未配对记录数}
"""
# 查询指定日期的记录
current_records = db.query(TradeRecord).filter(
TradeRecord.trade_date == trade_date
).order_by(
TradeRecord.variety, TradeRecord.symbol, TradeRecord.trade_date, TradeRecord.trade_time
).all()
if not current_records:
return {"created": 0, "updated": 0, "unpaired": 0}
# 查询之前未配对的记录(跨日持仓)
# 找出所有已经配对的记录 ID
all_pairs = db.query(TradePair).all()
paired_ids = set()
for p in all_pairs:
paired_ids.update(p.open_record_ids or [])
paired_ids.update(p.close_record_ids or [])
# 查询 trade_date 之前的未配对记录
previous_unpaired = db.query(TradeRecord).filter(
TradeRecord.trade_date < trade_date,
TradeRecord.id.notin_(paired_ids) if paired_ids else True,
).order_by(
TradeRecord.variety, TradeRecord.symbol, TradeRecord.trade_date, TradeRecord.trade_time
).all()
# 合并当前记录和之前未配对记录
all_records = previous_unpaired + current_records
# 按品种分组
by_variety = defaultdict(list)
for r in all_records:
by_variety[r.variety].append(r)
result = {"created": 0, "updated": 0, "unpaired": 0}
for variety, recs in by_variety.items():
pair_result = _pair_single_variety(db, variety, recs)
result["created"] += pair_result["created"]
result["updated"] += pair_result["updated"]
result["unpaired"] += pair_result["unpaired"]
return result
def _pair_single_variety(db: Session, variety: str, records: list) -> dict:
"""单个品种的配对逻辑"""
# 分离开仓和平仓
opens = [r for r in records if r.offset == '']
closes = [r for r in records if r.offset == '']
result = {"created": 0, "updated": 0, "unpaired": 0}
# 按方向分组
long_opens = [r for r in opens if r.direction == '']
short_opens = [r for r in opens if r.direction == '']
long_closes = [r for r in closes if r.direction == ''] # 卖平 = 多头平仓
short_closes = [r for r in closes if r.direction == ''] # 买平 = 空头平仓
# 多头配对
result["created"] += _pair_direction(db, variety, long_opens, long_closes, "long")
# 空头配对
result["created"] += _pair_direction(db, variety, short_opens, short_closes, "short")
# 统计未配对
all_paired_open_ids = set()
all_paired_close_ids = set()
existing_pairs = db.query(TradePair).filter(
TradePair.variety == variety,
).all()
for p in existing_pairs:
all_paired_open_ids.update(p.open_record_ids or [])
all_paired_close_ids.update(p.close_record_ids or [])
unpaired_count = 0
for r in records:
if r.id not in all_paired_open_ids and r.id not in all_paired_close_ids:
unpaired_count += 1
result["unpaired"] = unpaired_count
return result
def _pair_direction(db: Session, variety: str, opens: list, closes: list, direction: str) -> int:
"""单方向配对FIFO支持多开合并返回新建配对数
每个平仓记录会尽可能与前面的开仓记录配对形成一个完整配对
如果多个开仓合并才能匹配一个平仓则这些开仓记录会归入同一个配对
"""
created = 0
# 开仓队列,每个元素是 (record, remaining_volume)
open_queue = [(r, r.volume or 0) for r in opens]
close_queue = [(r, r.volume or 0) for r in closes]
while open_queue and close_queue:
close_rec, close_remaining = close_queue[0]
# 从开仓队列中取足够手数来匹配这个平仓
needed = close_remaining
used_opens = [] # (record, take_volume)
while needed > 0 and open_queue:
open_rec, open_remaining = open_queue[0]
take = min(open_remaining, needed)
used_opens.append((open_rec, take))
needed -= take
if take >= open_remaining:
open_queue.pop(0)
else:
open_queue[0] = (open_rec, open_remaining - take)
if not used_opens:
break
# 实际匹配的手数
matched_volume = close_remaining - needed
# 更新平仓队列
if matched_volume >= close_remaining:
close_queue.pop(0)
else:
close_queue[0] = (close_rec, close_remaining - matched_volume)
# 计算均价(按手数加权)
open_volume_sum = sum(take for _, take in used_opens)
open_price = sum((r.price or 0) * take for r, take in used_opens) / open_volume_sum if open_volume_sum > 0 else 0
close_price = close_rec.price or 0
# 计算盈亏和手续费
# 平仓盈亏按比例分配到该平仓记录
close_pnl = (close_rec.close_pnl or 0) * (matched_volume / close_rec.volume) if close_rec.volume else 0
total_commission = sum((r.commission or 0) * (take / r.volume) for r, take in used_opens if r.volume) + \
(close_rec.commission or 0) * (matched_volume / close_rec.volume) if close_rec.volume else 0
net_pnl = close_pnl - total_commission
# 最早开仓日期和最早平仓日期
open_date = min(r.trade_date for r, _ in used_opens)
close_date = close_rec.trade_date
pair = TradePair(
variety=variety,
direction=direction,
open_record_ids=[r.id for r, _ in used_opens],
close_record_ids=[close_rec.id],
open_price=round(open_price, 2),
close_price=round(close_price, 2),
total_volume=matched_volume,
close_pnl=round(close_pnl, 2),
total_commission=round(total_commission, 2),
net_pnl=round(net_pnl, 2),
open_date=open_date,
close_date=close_date,
)
db.add(pair)
created += 1
db.flush()
return created
def get_daily_trades_with_pairs(db: Session, trade_date: str) -> dict:
"""
获取指定日期的交易数据包含配对信息和反思状态
返回{date, stats, pairs, unpaired_records, daily_reflection}
"""
# 获取所有配对
pairs = db.query(TradePair).filter(
(TradePair.open_date == trade_date) | (TradePair.close_date == trade_date)
).order_by(TradePair.open_date, TradePair.close_date).all()
# 获取所有交易记录
records = db.query(TradeRecord).filter(
TradeRecord.trade_date == trade_date
).order_by(TradeRecord.trade_date, TradeRecord.trade_time).all()
# 计算统计
total_pnl = sum(p.net_pnl or 0 for p in pairs)
total_trades = len(pairs) + len(records) # 配对 + 未配对
winning_trades = sum(1 for p in pairs if (p.net_pnl or 0) > 0)
win_rate = round(winning_trades / total_trades * 100, 1) if total_trades > 0 else 0
# 获取反思状态
pair_ids = [p.id for p in pairs]
reflections = db.query(TradeReflection).filter(
TradeReflection.trade_pair_id.in_(pair_ids)
).all()
reflection_map = {r.trade_pair_id: r for r in reflections}
# 获取当日反思
daily_reflection = db.query(DailyReflection).filter(
DailyReflection.reflection_date == trade_date
).first()
# 构建返回数据
pairs_data = []
for p in pairs:
reflection = reflection_map.get(p.id)
pairs_data.append({
"id": p.id,
"variety": p.variety,
"direction": p.direction,
"open_price": p.open_price,
"close_price": p.close_price,
"total_volume": p.total_volume,
"close_pnl": p.close_pnl,
"total_commission": p.total_commission,
"net_pnl": p.net_pnl,
"open_date": p.open_date,
"close_date": p.close_date,
"open_record_ids": p.open_record_ids,
"close_record_ids": p.close_record_ids,
"reflection_status": "done" if reflection else ("pending" if _has_unpaired_records(db, p) else "none"),
"ai_analyzed": reflection.ai_analyzed if reflection else False,
"ai_version": reflection.ai_version if reflection else None,
})
# 未配对记录
paired_record_ids = set()
for p in pairs:
paired_record_ids.update(p.open_record_ids or [])
paired_record_ids.update(p.close_record_ids or [])
unpaired = [r for r in records if r.id not in paired_record_ids]
unpaired_data = [{
"id": r.id,
"symbol": r.symbol,
"variety": r.variety,
"direction": r.direction,
"offset": r.offset,
"price": r.price,
"volume": r.volume,
"trade_date": r.trade_date,
"trade_time": r.trade_time,
} for r in unpaired]
return {
"date": trade_date,
"stats": {
"total_pnl": round(total_pnl, 2),
"total_trades": total_trades,
"winning_trades": winning_trades,
"win_rate": win_rate,
"paired_count": len(pairs),
"unpaired_count": len(unpaired),
"reflected_count": sum(1 for r in reflection_map.values()),
},
"pairs": pairs_data,
"unpaired_records": unpaired_data,
"daily_reflection": {
"id": daily_reflection.id if daily_reflection else None,
"emotion_state": daily_reflection.emotion_state if daily_reflection else None,
"market_judgment": daily_reflection.market_judgment if daily_reflection else None,
"discipline_score": daily_reflection.discipline_score if daily_reflection else None,
"overall_rating": daily_reflection.overall_rating if daily_reflection else None,
"summary": daily_reflection.summary if daily_reflection else None,
"improvements": daily_reflection.improvements if daily_reflection else None,
} if daily_reflection else None,
}
def _has_unpaired_records(db: Session, pair: TradePair) -> bool:
"""检查配对是否有未配对的开仓或平仓记录"""
# 简化逻辑:如果开仓日期和平仓日期不同,可能存在跨日未配对
return pair.open_date != pair.close_date
def create_manual_pair(db: Session, open_record_ids: list[int], close_record_ids: list[int]) -> TradePair:
"""手动创建配对"""
open_records = db.query(TradeRecord).filter(TradeRecord.id.in_(open_record_ids)).all()
close_records = db.query(TradeRecord).filter(TradeRecord.id.in_(close_record_ids)).all()
if not open_records or not close_records:
raise ValueError("开仓或平仓记录不存在")
variety = open_records[0].variety
direction = "long" if open_records[0].direction == '' else "short"
# 计算统计
total_volume = sum(r.volume or 0 for r in open_records)
total_close_pnl = sum(r.close_pnl or 0 for r in close_records)
total_commission = sum((r.commission or 0) for r in open_records) + sum((r.commission or 0) for r in close_records)
net_pnl = total_close_pnl - total_commission
avg_open_price = sum(r.price or 0 for r in open_records) / len(open_records) if open_records else 0
avg_close_price = sum(r.price or 0 for r in close_records) / len(close_records) if close_records else 0
pair = TradePair(
variety=variety,
direction=direction,
open_record_ids=open_record_ids,
close_record_ids=close_record_ids,
open_price=round(avg_open_price, 2),
close_price=round(avg_close_price, 2),
total_volume=total_volume,
close_pnl=total_close_pnl,
total_commission=total_commission,
net_pnl=round(net_pnl, 2),
open_date=open_records[0].trade_date,
close_date=close_records[-1].trade_date,
)
db.add(pair)
db.flush()
return pair
def delete_pair(db: Session, pair_id: int) -> bool:
"""删除配对(不删除原始交易记录)"""
pair = db.query(TradePair).filter(TradePair.id == pair_id).first()
if not pair:
return False
# 删除关联的反思
db.query(TradeReflection).filter(TradeReflection.trade_pair_id == pair_id).delete()
# 删除配对
db.delete(pair)
db.flush()
return True

@ -495,6 +495,7 @@
.tr-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }
.tr-toolbar-left, .tr-toolbar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tr-btn { padding: 10px 20px; border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; display: flex; align-items: center; gap: 8px; transition: all 0.2s; font-family: inherit; }
.tr-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.tr-btn-primary { background: var(--color-brand); color: #fff; }
.tr-btn-primary:hover { background: #0066d6; box-shadow: 0 4px 10px rgba(0,122,255,0.3); }
.tr-btn-secondary { background: #fff; color: var(--text-secondary); box-shadow: var(--shadow-sm); border: 1px solid rgba(0,0,0,0.05); }
@ -582,12 +583,146 @@
.tr-empty-icon { font-size: 56px; opacity: 0.3; }
.tr-empty-text { font-size: 15px; color: var(--text-tertiary); }
/* ============================================
交易反思子系统样式
============================================ */
.tr-subtabs { display: flex; gap: 8px; margin-bottom: 24px; background: rgba(255,255,255,0.6); padding: 4px; border-radius: 14px; backdrop-filter: blur(8px); width: fit-content; }
.tr-subtab { padding: 8px 20px; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; background: transparent; color: var(--text-secondary); transition: all 0.2s; font-family: inherit; }
.tr-subtab:hover { background: rgba(0,0,0,0.03); color: var(--text-primary); }
.tr-subtab.active { background: #fff; color: var(--color-brand); box-shadow: var(--shadow-sm); font-weight: 600; }
.tr-tab-panel { display: none; }
.tr-tab-panel.active { display: block; }
/* 交易反思 - 日期导航 */
.tr-refl-datebar { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; }
.tr-refl-datebtn { width: 36px; height: 36px; border-radius: 50%; border: none; background: #fff; color: var(--text-secondary); cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.2s; font-size: 16px; }
.tr-refl-datebtn:hover { background: var(--color-brand); color: #fff; }
.tr-refl-datetext { font-size: 20px; font-weight: 700; color: var(--text-primary); min-width: 120px; text-align: center; }
.tr-refl-actions { display: flex; gap: 10px; margin-left: auto; }
/* 反思统计卡片 */
.tr-refl-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.tr-refl-statcard { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; text-align: center; box-shadow: var(--shadow-sm); transition: transform 0.2s; }
.tr-refl-statcard:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-refl-statlabel { font-size: 12px; color: var(--text-tertiary); margin-bottom: 8px; font-weight: 600; }
.tr-refl-statvalue { font-size: 26px; font-weight: 700; }
.tr-refl-statvalue.profit { color: var(--color-down); }
.tr-refl-statvalue.loss { color: var(--color-up); }
.tr-refl-reflected { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 600; }
.tr-refl-reflected.yes { background: rgba(52,199,89,0.12); color: var(--color-down); }
.tr-refl-reflected.no { background: rgba(255,59,48,0.12); color: var(--color-up); }
/* 当日反思区域 */
.tr-refl-daily { background: var(--bg-card); border-radius: var(--radius-card); padding: 24px; margin-bottom: 24px; box-shadow: var(--shadow-sm); }
.tr-refl-daily-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.tr-refl-daily-title { font-size: 17px; font-weight: 700; color: var(--text-primary); }
.tr-refl-daily-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.tr-refl-daily-item { background: #F5F5F7; border-radius: 12px; padding: 14px; }
.tr-refl-daily-label { font-size: 11px; color: var(--text-tertiary); margin-bottom: 6px; font-weight: 600; text-transform: uppercase; }
.tr-refl-daily-value { font-size: 14px; color: var(--text-primary); font-weight: 500; line-height: 1.4; }
.tr-refl-daily-value.empty { color: var(--text-tertiary); font-style: italic; }
/* 交易配对卡片 */
.tr-refl-section-title { font-size: 17px; font-weight: 700; margin-bottom: 16px; color: var(--text-primary); }
.tr-refl-pairs { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; margin-bottom: 24px; }
.tr-refl-card { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); transition: transform 0.2s, box-shadow 0.2s; position: relative; overflow: hidden; }
.tr-refl-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-refl-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--color-brand); }
.tr-refl-card.profit::before { background: var(--color-down); }
.tr-refl-card.loss::before { background: var(--color-up); }
.tr-refl-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.tr-refl-card-symbol { font-size: 16px; font-weight: 700; }
.tr-refl-card-dir { font-size: 12px; padding: 3px 10px; border-radius: 6px; font-weight: 600; }
.tr-refl-card-dir.long { background: rgba(52,199,89,0.12); color: var(--color-down); }
.tr-refl-card-dir.short { background: rgba(255,59,48,0.12); color: var(--color-up); }
.tr-refl-card-pnl { font-size: 20px; font-weight: 700; text-align: right; }
.tr-refl-card-pnl.profit { color: var(--color-down); }
.tr-refl-card-pnl.loss { color: var(--color-up); }
.tr-refl-card-nodes { font-size: 13px; color: var(--text-secondary); margin-bottom: 14px; line-height: 1.6; }
.tr-refl-card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.tr-refl-tag { font-size: 11px; padding: 4px 10px; border-radius: 9999px; background: #F5F5F7; color: var(--text-secondary); font-weight: 500; }
.tr-refl-tag.active { background: rgba(0,122,255,0.12); color: var(--color-brand); }
.tr-refl-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.tr-refl-card-actions .tr-btn { padding: 6px 12px; font-size: 12px; }
/* 未配对交易 */
.tr-refl-unpaired { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); }
.tr-refl-unpaired-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.tr-refl-unpaired-header .tr-refl-section-title { margin-bottom: 0; }
.tr-refl-unpaired-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tr-refl-unpaired-table th { background: #F5F5F7; padding: 10px 8px; text-align: left; font-weight: 600; color: var(--text-tertiary); font-size: 12px; }
.tr-refl-unpaired-table td { padding: 10px 8px; border-bottom: 1px solid rgba(0,0,0,0.04); color: var(--text-primary); }
.tr-refl-unpaired-table input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--color-brand); cursor: pointer; }
/* 经验库 */
.tr-exp-toolbar { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.tr-exp-search { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 12px; padding: 0 14px; height: 40px; font-size: 14px; width: 260px; outline: none; }
.tr-exp-search:focus { border-color: var(--color-brand); box-shadow: 0 0 0 3px rgba(0,122,255,0.15); }
.tr-exp-filter { background: #fff; border: 1px solid rgba(0,0,0,0.05); border-radius: 10px; padding: 0 12px; height: 36px; font-size: 13px; color: var(--text-primary); outline: none; }
.tr-exp-viewtabs { display: flex; gap: 6px; margin-left: auto; }
.tr-exp-viewtab { padding: 6px 14px; border-radius: 10px; font-size: 13px; cursor: pointer; border: none; background: #fff; color: var(--text-secondary); transition: all 0.2s; }
.tr-exp-viewtab:hover { background: #F5F5F7; }
.tr-exp-viewtab.active { background: var(--color-brand); color: #fff; }
.tr-exp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
.tr-exp-card { background: var(--bg-card); border-radius: var(--radius-card); padding: 20px; box-shadow: var(--shadow-sm); transition: all 0.2s; }
.tr-exp-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.tr-exp-card-title { font-size: 15px; font-weight: 700; margin-bottom: 10px; color: var(--text-primary); }
.tr-exp-card-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 12px; }
.tr-exp-card-content { font-size: 13px; color: var(--text-secondary); line-height: 1.7; margin-bottom: 14px; }
.tr-exp-card-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tr-exp-pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 24px; }
.tr-exp-pageinfo { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
.tr-exp-detail-meta { font-size: 12px; color: var(--text-tertiary); margin-bottom: 16px; }
.tr-exp-detail-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; background: #F5F5F7; border-radius: 12px; padding: 16px; margin-bottom: 16px; white-space: pre-wrap; }
.tr-exp-detail-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.tr-exp-detail-source { font-size: 13px; color: var(--text-secondary); }
.tr-exp-detail-source .link { cursor: pointer; }
/* Modal 表单 */
.tr-form-group { margin-bottom: 18px; }
.tr-form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
.tr-form-input, .tr-form-select, .tr-form-textarea { width: 100%; background: #F5F5F7; border: 1px solid transparent; border-radius: 10px; padding: 10px 12px; font-size: 14px; color: var(--text-primary); font-family: inherit; outline: none; transition: all 0.2s; }
.tr-form-input:focus, .tr-form-select:focus, .tr-form-textarea:focus { border-color: var(--color-brand); background: #fff; box-shadow: 0 0 0 3px rgba(0,122,255,0.1); }
.tr-form-textarea { min-height: 90px; resize: vertical; }
.tr-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.tr-form-rating { display: flex; gap: 6px; }
.tr-form-star { font-size: 22px; color: #E5E5E7; cursor: pointer; transition: color 0.15s; }
.tr-form-star.active { color: #FF9F0A; }
.tr-form-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
/* 标签管理 */
.tr-tag-section { margin-bottom: 18px; }
.tr-tag-section-title { font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 10px; }
.tr-tag-pool { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.tr-tag-chip { padding: 6px 12px; border-radius: 9999px; font-size: 12px; cursor: pointer; border: 1px solid rgba(0,0,0,0.06); background: #fff; color: var(--text-secondary); transition: all 0.15s; }
.tr-tag-chip:hover { border-color: var(--color-brand); color: var(--color-brand); }
.tr-tag-chip.selected { background: rgba(0,122,255,0.12); border-color: var(--color-brand); color: var(--color-brand); font-weight: 500; }
.tr-tag-custom { display: flex; gap: 8px; margin-top: 12px; }
.tr-tag-custom .tr-form-input { flex: 1; }
/* AI 分析结果 */
.tr-ai-version-row { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; padding: 12px 16px; background: #F5F5F7; border-radius: 12px; }
.tr-ai-version-label { font-size: 13px; font-weight: 600; color: var(--text-tertiary); }
.tr-ai-version-row .tr-form-select { flex: 1; min-width: 200px; margin: 0; }
.tr-ai-result-section { margin-bottom: 18px; }
.tr-ai-result-title { font-size: 14px; font-weight: 700; color: var(--color-brand); margin-bottom: 8px; }
.tr-ai-result-content { font-size: 14px; color: var(--text-primary); line-height: 1.8; background: #F5F5F7; border-radius: 12px; padding: 14px; }
.tr-ai-list { padding-left: 18px; margin: 0; }
.tr-ai-list li { margin-bottom: 6px; line-height: 1.6; }
@media (max-width: 1200px) {
.tr-stats-grid { grid-template-columns: repeat(3, 1fr); }
.tr-refl-stats { grid-template-columns: repeat(2, 1fr); }
.tr-refl-daily-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.tr-stats-grid { grid-template-columns: repeat(2, 1fr); }
.tr-overview-grid { grid-template-columns: repeat(2, 1fr); }
.tr-refl-stats { grid-template-columns: 1fr; }
.tr-refl-daily-grid { grid-template-columns: 1fr; }
.tr-form-row { grid-template-columns: 1fr; }
.tr-refl-pairs { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
@ -974,82 +1109,429 @@
<!-- 交易复盘视图 -->
<div id="trade-review-view" class="view">
<!-- 工具栏 -->
<div class="tr-toolbar">
<div class="tr-toolbar-left">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" />
<input type="file" id="tr-batch-file-input" accept=".xls,.xlsx" multiple style="display:none" />
<button id="tr-btn-import" class="tr-btn tr-btn-primary">
<span>📥</span><span>导入结算单</span>
</button>
<button id="tr-btn-batch-import" class="tr-btn tr-btn-primary" title="同时导入多个结算单文件">
<span>📂</span><span>批量导入</span>
</button>
<input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
<button id="tr-btn-query" class="tr-btn tr-btn-secondary">查询</button>
<!-- 子 Tab 切换栏 -->
<div class="tr-subtabs" id="tr-subtabs">
<button class="tr-subtab active" data-subtab="stats">统计分析</button>
<button class="tr-subtab" data-subtab="reflection">交易反思</button>
<button class="tr-subtab" data-subtab="experience">经验库</button>
</div>
<!-- 统计分析子 Tab -->
<div id="tr-tab-stats" class="tr-tab-panel active">
<!-- 工具栏 -->
<div class="tr-toolbar">
<div class="tr-toolbar-left">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" />
<input type="file" id="tr-batch-file-input" accept=".xls,.xlsx" multiple style="display:none" />
<button id="tr-btn-import" class="tr-btn tr-btn-primary">
<span>📥</span><span>导入结算单</span>
</button>
<button id="tr-btn-batch-import" class="tr-btn tr-btn-primary" title="同时导入多个结算单文件">
<span>📂</span><span>批量导入</span>
</button>
<input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
<button id="tr-btn-query" class="tr-btn tr-btn-secondary">查询</button>
</div>
<div class="tr-toolbar-right">
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除当前查询日期的所有交易记录">
<span>🗑️</span><span>删除当日</span>
</button>
<button id="tr-btn-batches" class="tr-btn tr-btn-secondary">
<span>📋</span><span>批次管理</span>
</button>
</div>
</div>
<div class="tr-toolbar-right">
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除当前查询日期的所有交易记录">
<span>🗑️</span><span>删除当日</span>
</button>
<button id="tr-btn-batches" class="tr-btn tr-btn-secondary">
<span>📋</span><span>批次管理</span>
</button>
<!-- 6个统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></div>
<!-- 账户权益曲线 -->
<div class="tr-chart-box">
<div class="tr-chart-title">账户权益曲线(累计净盈亏 + 每日盈亏)</div>
<div class="tr-chart-container" id="tr-equity-chart"></div>
</div>
<!-- 品种盈亏分布气泡图 -->
<div class="tr-chart-box">
<div class="tr-chart-title">品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)</div>
<div class="tr-bubble-container" id="tr-bubble-chart"></div>
</div>
<!-- 每日交易详情表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">每日交易详情(点击日期查看详情)</div>
<table class="tr-table">
<thead>
<tr>
<th>日期</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th>
<th>净盈亏</th><th>盈利笔</th><th>亏损笔</th><th>胜率</th>
<th>最大盈利</th><th>最大亏损</th><th>品种数</th><th>操作</th>
</tr>
</thead>
<tbody id="tr-daily-body"></tbody>
</table>
<div id="tr-daily-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📊</div>
<div class="tr-empty-text">暂无交易数据,请先导入结算单</div>
</div>
</div>
<!-- 品种盈亏排行表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">品种盈亏排行(点击查看品种详情)</div>
<table class="tr-table">
<thead>
<tr>
<th>排名</th><th>代码</th><th>名称</th><th>净盈亏</th>
<th>平仓盈亏</th><th>手续费</th><th>胜率</th>
<th>盈亏比</th><th>笔数</th><th>操作</th>
</tr>
</thead>
<tbody id="tr-variety-body"></tbody>
</table>
<div id="tr-variety-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📋</div>
<div class="tr-empty-text">暂无品种数据</div>
</div>
</div>
</div>
<!-- 交易反思子 Tab -->
<div id="tr-tab-reflection" class="tr-tab-panel">
<div class="tr-refl-datebar">
<button class="tr-refl-datebtn" id="tr-refl-prev" title="前一天"></button>
<span class="tr-refl-datetext" id="tr-refl-current-date">-</span>
<button class="tr-refl-datebtn" id="tr-refl-next" title="后一天"></button>
<div class="tr-refl-actions">
<button class="tr-btn tr-btn-secondary" id="tr-refl-edit-daily">
<span>✏️</span><span>当日反思编辑</span>
</button>
</div>
</div>
<div class="tr-refl-stats">
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">总盈亏</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-pnl">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">交易笔数</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-trades">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">胜率</div>
<div class="tr-refl-statvalue" id="tr-refl-stat-winrate">-</div>
</div>
<div class="tr-refl-statcard">
<div class="tr-refl-statlabel">已反思</div>
<div id="tr-refl-stat-reflected">-</div>
</div>
</div>
<div class="tr-refl-daily">
<div class="tr-refl-daily-header">
<div class="tr-refl-daily-title">当日反思</div>
</div>
<div class="tr-refl-daily-grid">
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">情绪状态</div>
<div class="tr-refl-daily-value" id="tr-refl-emotion">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">执行纪律</div>
<div class="tr-refl-daily-value" id="tr-refl-discipline">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">总体评价</div>
<div class="tr-refl-daily-value" id="tr-refl-overall">-</div>
</div>
<div class="tr-refl-daily-item">
<div class="tr-refl-daily-label">市场判断</div>
<div class="tr-refl-daily-value" id="tr-refl-market">-</div>
</div>
</div>
</div>
<div class="tr-refl-section-title">交易配对</div>
<div id="tr-refl-pairs" class="tr-refl-pairs"></div>
<div class="tr-refl-unpaired">
<div class="tr-refl-unpaired-header">
<div class="tr-refl-section-title">未配对交易</div>
<button class="tr-btn tr-btn-primary" id="tr-refl-manual-pair-btn">
<span>🔗</span><span>手动配对</span>
</button>
</div>
<table class="tr-refl-unpaired-table">
<thead>
<tr><th><input type="checkbox" id="tr-refl-unpaired-select-all" title="全选"></th><th>时间</th><th>品种</th><th>开平</th><th>方向</th><th>价格</th><th>手数</th><th>盈亏</th></tr>
</thead>
<tbody id="tr-refl-unpaired-body"></tbody>
</table>
<div id="tr-refl-unpaired-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-text">暂无未配对交易</div>
</div>
</div>
</div>
<!-- 6个统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></div>
<!-- 经验库子 Tab -->
<div id="tr-tab-experience" class="tr-tab-panel">
<div class="tr-exp-toolbar">
<input type="text" class="tr-exp-search" id="tr-exp-search" placeholder="搜索经验...">
<select class="tr-exp-filter" id="tr-exp-tag-filter">
<option value="">全部标签</option>
</select>
<select class="tr-exp-filter" id="tr-exp-type-filter">
<option value="">全部类型</option>
<option value="lesson">教训</option>
<option value="insight">心得</option>
<option value="strategy">策略</option>
<option value="discipline">纪律</option>
</select>
<div class="tr-exp-viewtabs">
<button class="tr-exp-viewtab active" data-view="list">列表</button>
<button class="tr-exp-viewtab" data-view="timeline">时间线</button>
<button class="tr-exp-viewtab" data-view="category">分类</button>
</div>
</div>
<div id="tr-exp-container" class="tr-exp-grid"></div>
<div id="tr-exp-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📚</div>
<div class="tr-empty-text">暂无经验记录</div>
</div>
<div class="tr-exp-pagination" id="tr-exp-pagination" style="display:none;">
<button class="tr-btn tr-btn-secondary" id="tr-exp-prev">上一页</button>
<span class="tr-exp-pageinfo" id="tr-exp-pageinfo"></span>
<button class="tr-btn tr-btn-secondary" id="tr-exp-next">下一页</button>
</div>
</div>
<!-- 账户权益曲线 -->
<div class="tr-chart-box">
<div class="tr-chart-title">账户权益曲线(累计净盈亏 + 每日盈亏)</div>
<div class="tr-chart-container" id="tr-equity-chart"></div>
<!-- 反思编辑 Modal -->
<div id="tr-refl-edit-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseEditModal()">
<div class="tr-modal" style="max-width:720px;">
<div class="tr-modal-header">
<span class="tr-modal-title">写反思</span>
<button class="tr-modal-close" onclick="trReflCloseEditModal()"></button>
</div>
<div class="tr-modal-body">
<form id="tr-refl-edit-form">
<input type="hidden" id="tr-refl-edit-pair-id">
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">入场理由</label>
<textarea class="tr-form-textarea" id="tr-refl-entry-reason" placeholder="当时为什么入场?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">出场理由</label>
<textarea class="tr-form-textarea" id="tr-refl-exit-reason" placeholder="为什么在这个位置出场?"></textarea>
</div>
</div>
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">入场时机评价</label>
<select class="tr-form-select" id="tr-refl-entry-timing">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">出场时机评价</label>
<select class="tr-form-select" id="tr-refl-exit-timing">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">仓位管理反思</label>
<textarea class="tr-form-textarea" id="tr-refl-position" placeholder="仓位是否合理?是否有改进空间?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">纪律评分</label>
<div class="tr-form-rating" id="tr-refl-discipline-score">
<span class="tr-form-star" data-score="1"></span>
<span class="tr-form-star" data-score="2"></span>
<span class="tr-form-star" data-score="3"></span>
<span class="tr-form-star" data-score="4"></span>
<span class="tr-form-star" data-score="5"></span>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">自由反思</label>
<textarea class="tr-form-textarea" id="tr-refl-free" placeholder="还有什么想记录的?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">标签</label>
<div id="tr-refl-edit-tags" class="tr-tag-pool"></div>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseEditModal()">取消</button>
<button type="submit" class="tr-btn tr-btn-primary">保存反思</button>
</div>
</form>
</div>
</div>
</div>
<!-- 品种盈亏分布气泡图 -->
<div class="tr-chart-box">
<div class="tr-chart-title">品种盈亏分布(气泡大小=手续费占比,颜色=净盈亏)</div>
<div class="tr-bubble-container" id="tr-bubble-chart"></div>
<!-- 当日反思编辑 Modal -->
<div id="tr-refl-daily-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseDailyModal()">
<div class="tr-modal" style="max-width:640px;">
<div class="tr-modal-header">
<span class="tr-modal-title">当日反思编辑</span>
<button class="tr-modal-close" onclick="trReflCloseDailyModal()"></button>
</div>
<div class="tr-modal-body">
<form id="tr-refl-daily-form">
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">情绪状态</label>
<select class="tr-form-select" id="tr-refl-daily-emotion">
<option value="">请选择</option>
<option value="calm">平静</option>
<option value="confident">自信</option>
<option value="anxious">焦虑</option>
<option value="greedy">贪婪</option>
<option value="fearful">恐惧</option>
<option value="impulsive">冲动</option>
<option value="tired">疲惫</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">执行纪律评分</label>
<div class="tr-form-rating" id="tr-refl-daily-discipline-score">
<span class="tr-form-star" data-score="1"></span>
<span class="tr-form-star" data-score="2"></span>
<span class="tr-form-star" data-score="3"></span>
<span class="tr-form-star" data-score="4"></span>
<span class="tr-form-star" data-score="5"></span>
</div>
</div>
</div>
<div class="tr-form-row">
<div class="tr-form-group">
<label class="tr-form-label">总体评价</label>
<select class="tr-form-select" id="tr-refl-daily-overall">
<option value="">请选择</option>
<option value="excellent">优秀</option>
<option value="good">良好</option>
<option value="average">一般</option>
<option value="poor">较差</option>
<option value="bad">糟糕</option>
</select>
</div>
<div class="tr-form-group">
<label class="tr-form-label">市场判断</label>
<select class="tr-form-select" id="tr-refl-daily-market">
<option value="">请选择</option>
<option value="accurate">准确</option>
<option value="basically">基本准确</option>
<option value="deviated">有所偏差</option>
<option value="wrong">错误</option>
</select>
</div>
</div>
<div class="tr-form-group">
<label class="tr-form-label">总结</label>
<textarea class="tr-form-textarea" id="tr-refl-daily-summary" placeholder="今天交易的最大收获是什么?"></textarea>
</div>
<div class="tr-form-group">
<label class="tr-form-label">改进方向</label>
<textarea class="tr-form-textarea" id="tr-refl-daily-improvement" placeholder="明天要注意什么?"></textarea>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseDailyModal()">取消</button>
<button type="submit" class="tr-btn tr-btn-primary">保存当日反思</button>
</div>
</form>
</div>
</div>
</div>
<!-- 每日交易详情表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">每日交易详情(点击日期查看详情)</div>
<table class="tr-table">
<thead>
<tr>
<th>日期</th><th>笔数</th><th>平仓盈亏</th><th>手续费</th>
<th>净盈亏</th><th>盈利笔</th><th>亏损笔</th><th>胜率</th>
<th>最大盈利</th><th>最大亏损</th><th>品种数</th>
</tr>
</thead>
<tbody id="tr-daily-body"></tbody>
</table>
<div id="tr-daily-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📊</div>
<div class="tr-empty-text">暂无交易数据,请先导入结算单</div>
<!-- 标签管理 Modal -->
<div id="tr-refl-tags-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseTagsModal()">
<div class="tr-modal" style="max-width:560px;">
<div class="tr-modal-header">
<span class="tr-modal-title">标签管理</span>
<button class="tr-modal-close" onclick="trReflCloseTagsModal()"></button>
</div>
<div class="tr-modal-body">
<input type="hidden" id="tr-refl-tags-pair-id">
<div id="tr-refl-tags-preset"></div>
<div class="tr-tag-custom">
<input type="text" class="tr-form-input" id="tr-refl-tag-input" placeholder="输入自定义标签">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflAddCustomTag()">添加</button>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseTagsModal()">取消</button>
<button type="button" class="tr-btn tr-btn-primary" onclick="trReflSaveTags()">保存标签</button>
</div>
</div>
</div>
</div>
<!-- 品种盈亏排行表 -->
<div class="tr-tbl-box">
<div class="tr-tbl-title">品种盈亏排行(点击查看品种详情)</div>
<table class="tr-table">
<thead>
<tr>
<th>排名</th><th>代码</th><th>名称</th><th>净盈亏</th>
<th>平仓盈亏</th><th>手续费</th><th>胜率</th>
<th>盈亏比</th><th>笔数</th><th>操作</th>
</tr>
</thead>
<tbody id="tr-variety-body"></tbody>
</table>
<div id="tr-variety-empty" class="tr-empty" style="display:none;">
<div class="tr-empty-icon">📋</div>
<div class="tr-empty-text">暂无品种数据</div>
<!-- 经验详情 Modal -->
<div id="tr-exp-detail-modal" class="tr-modal-overlay" onclick="if(event.target===this)trExpCloseDetailModal()">
<div class="tr-modal" style="max-width:600px;">
<div class="tr-modal-header">
<span class="tr-modal-title" id="tr-exp-detail-title">经验详情</span>
<button class="tr-modal-close" onclick="trExpCloseDetailModal()"></button>
</div>
<div class="tr-modal-body">
<div class="tr-exp-detail-meta" id="tr-exp-detail-meta"></div>
<div class="tr-exp-detail-content" id="tr-exp-detail-content"></div>
<div class="tr-exp-detail-tags" id="tr-exp-detail-tags"></div>
<div class="tr-exp-detail-source" id="tr-exp-detail-source"></div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trExpCloseDetailModal()">关闭</button>
</div>
</div>
</div>
</div>
<!-- AI 分析结果 Modal -->
<div id="tr-refl-ai-modal" class="tr-modal-overlay" onclick="if(event.target===this)trReflCloseAIModal()">
<div class="tr-modal" style="max-width:680px;">
<div class="tr-modal-header">
<span class="tr-modal-title">AI 分析结果</span>
<button class="tr-modal-close" onclick="trReflCloseAIModal()"></button>
</div>
<div class="tr-modal-body">
<input type="hidden" id="tr-refl-ai-pair-id">
<div class="tr-ai-version-row">
<label class="tr-ai-version-label" for="tr-refl-ai-version">版本</label>
<select class="tr-form-select" id="tr-refl-ai-version"></select>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">综合评价</div>
<div class="tr-ai-result-content" id="tr-refl-ai-overall">-</div>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">优点</div>
<ul class="tr-ai-list" id="tr-refl-ai-strengths"></ul>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">不足</div>
<ul class="tr-ai-list" id="tr-refl-ai-weaknesses"></ul>
</div>
<div class="tr-ai-result-section">
<div class="tr-ai-result-title">经验提炼建议</div>
<div class="tr-ai-result-content" id="tr-refl-ai-suggestion">-</div>
</div>
<div class="tr-form-actions">
<button type="button" class="tr-btn tr-btn-secondary" onclick="trReflCloseAIModal()">关闭</button>
<button type="button" class="tr-btn tr-btn-primary" onclick="trReflSaveExperience()">保存到经验库</button>
</div>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,322 @@
# 期货智析futures_analysis模块 — 功能与逻辑文档
## 一、模块概述
期货智析模块是一个集**数据采集、技术分析、AI智能分析、复盘计划生成、交易复盘**于一体的期货分析平台。采用前后端分离架构,前端为 SPA 单页应用,后端提供 RESTful API。
---
## 二、文件结构
### 后端
| 文件 | 说明 |
|------|------|
| `app/api/futures_analysis.py` | 主 API 路由(~970行前缀 `/api/v1/futures` |
| `app/api/review_plan.py` | 复盘计划 API前缀 `/api/v1/review` |
| `app/api/trade_review.py` | 交易复盘 API前缀 `/api/v1/trade-review` |
| `app/analysis_models.py` | 数据模型定义(~325行14 个 ORM 模型类 |
| `app/analysis_db.py` | 独立数据库初始化SQLite + MySQL 动态切换) |
| `app/services/ai_analysis.py` | AI 四维联合分析服务(~513行 |
| `app/services/cache.py` | 缓存服务SQLite/Redis/MySQL 三级存储) |
| `app/services/collector.py` | 数据采集服务(封装底层采集脚本) |
| `app/services/plan_generator.py` | 复盘计划生成器 |
| `app/services/trade_parser.py` | 结算单解析器 |
| `app/config_store.py` | 统一配置存储MySQL 优先 + JSON 文件兜底) |
### 前端
| 文件 | 说明 |
|------|------|
| `app/static/futures_analysis.html` | 主页面(~1232行 |
| `app/static/futures_analysis.js` | 前端逻辑(~3855行 |
| `app/static/futures_analysis.css` | 样式表(~3124行 |
### 数据库
| 文件 | 说明 |
|------|------|
| `data/futures_analysis.db` | SQLite 数据库(兜底存储) |
---
## 三、整体分析逻辑流程
### 3.1 数据流全链路
```
数据采集(collector.py) → 缓存存储(cache.py) → API接口(futures_analysis.py) → 前端渲染(JS)
AI分析服务(ai_analysis.py)
AI结果缓存(AIAnalysisCache表)
```
### 3.2 品种列表加载流程
1. **前端** `loadFuturesList()` 调用 `GET /api/v1/futures/list`
2. **后端** `get_futures_list()` 执行:
- 从 `config_store` 加载品种配置(品种名称 → 合约代码映射)
- 遍历每个品种,调用 `get_cached_data()` 从缓存获取 K 线数据
- 对每个品种计算:
- 当前价格、涨跌幅
- 操作建议(`_get_suggestion`
- 多周期趋势(`_get_period_trends`
- 成功率(`_calc_success_rate`
- 趋势评分(`_calc_trend_score`
- 压力支撑位Pivot Point 公式)
3. **前端** 渲染品种卡片网格,同时异步调用 `loadAllAIAnalysis()` 分批加载每个品种的 AI 分析结果
### 3.3 单品种详情分析流程
1. **前端** `showDetailView(symbol)` 切换到详情视图
2. 并行执行三个请求:
- `loadFuturesDetail(symbol)``GET /api/v1/futures/detail/{symbol}` — 详细行情和技术指标
- `loadKlineData(symbol, period)``GET /api/v1/futures/kline/{symbol}` — K 线数据(图表用)
- `loadHistoryListForAnalysis(symbol)``GET /api/v1/futures/ai-analysis/{symbol}/history` — AI 分析历史
3. **后端** `get_futures_detail()` 计算完整技术指标:
- **MACD**`_calc_macd`EMA12/EMA26 → DIF/DEA → 金叉/死叉信号
- **RSI**`_calc_rsi`14 周期 RSI → 超买/超卖/正常判断
- **布林带**`_calc_boll`20 周期 MA ± 2 倍标准差 → 上轨/中轨/下轨信号
- **KDJ**`_calc_kdj`9 周期 RSV → K/D 值 → 偏多/偏空/中性
- **Pivot Point**PP = (H+L+C)/3R1/R2/S1/S2 标准公式
4. **前端** 使用 ECharts 渲染 K 线图(含 MA5/MA10/MA20 均线、成交量柱、MACD 子图)
### 3.4 AI 智能分析流程(核心)
1. **前端** `runAIAnalysis()` 调用 `POST /api/v1/futures/ai-analysis/{symbol}`
2. **后端** `AIFuturesAnalyzer.analyze()` 执行完整流程:
```
步骤1: get_active_model()
└─ 从配置中获取当前激活的 AI 模型(支持 OpenAI 兼容接口)
步骤2: prepare_multi_period_data()
└─ 准备 5 个周期5min/15min/30min/60min/daily的 K 线数据
└─ 每个周期计算技术指标MA10/MA20/MACD/KDJ/量比等)
步骤3: AIAnalysisPrompt.build_prompt()
└─ 构建"四维联合判断分析法(4D-XV)"提示词
└─ 系统角色20年经验的资深金融分析师
└─ 要求输出 JSON 格式的四维分析结果
步骤4: call_ai_model()
└─ 通过 HTTP POST 调用 AI 模型OpenAI 兼容格式180秒超时
步骤5: parse_ai_response()
└─ 正则提取 JSON 响应
步骤6: save_analysis_cache()
└─ 保存结果到 AIAnalysisCache 表,附带 K 线时间戳
```
3. **智能缓存策略**`should_reanalyze`
- 分析超过 15 分钟 → 重新分析
- K 线数据时间戳比分析时间新 → 重新分析
- 否则返回缓存结果
4. **前端** `displayAIAnalysisResult()` 渲染分析结果,并通过 `syncAIToPanels()` 同步数据到各个面板
### 3.5 AI 分析输出结构4D-XV 四维联合分析)
AI 分析返回的 JSON 包含以下字段:
| 字段 | 说明 |
|------|------|
| `summary` | 分析摘要 |
| `four_dimensional` | 四维分析MACD趋势、成交量资金、KDJ时机、多周期方向 |
| `kdj_diagnosis` | KDJ 诊断详情 |
| `pivot_points` | 压力支撑位 |
| `red_lines_check` | 红线审查(不可违反的交易纪律) |
| `discipline_score` | 纪律评分 |
| `trading_suggestion` | 交易建议(方向、置信度、入场区间、止损、仓位) |
| `scenario_plans` | 情景预案(突破/震荡/反转/消息影响) |
| `risk_warnings` | 风险提示 |
| `experience_lessons` | 经验教训 |
---
## 四、全部功能清单
### 4.1 品种分析(主页面)
| 功能 | 描述 |
|------|------|
| 品种列表展示 | 卡片网格展示所有期货品种,含价格、涨跌、建议、成功率、趋势评分、压力支撑位 |
| 分类筛选 | 全部 / 能源 / 金属 / 农产品 / 金融 四大分类 |
| 搜索 | 按品种名称或合约代码搜索 |
| 趋势筛选 | 点击统计卡片按上涨 / 下跌 / 震荡筛选 |
| 数据刷新 | 单个品种刷新或全部刷新(异步后台执行,进度轮询) |
| 自选品种 | 添加/取消自选,独立自选页面 |
| AI 批量分析 | 一键对所有合约进行 AI 分析(分批 3 个并发2 秒间隔) |
### 4.2 详情分析页
| 功能 | 描述 |
|------|------|
| 行情概览 | 品种名称、合约代码、当前价、涨跌、开/高/低/量 |
| K 线图表 | ECharts 渲染,支持 5M/15M/30M/1H 四个周期切换,含 MA 均线、成交量、MACD 子图 |
| 技术指标面板 | MACD金叉/死叉、RSI超买/超卖、布林带、KDJ |
| 关键点位 | Pivot Point 计算的压力位(R1/R2)、支撑位(S1/S2)、中枢(PP) |
| 多周期趋势 | 5/15/30/60 分钟四个周期的趋势方向 |
| AI 思维分析 | 四维联合分析结果展示(摘要、方向、置信度、入场区间、止损、仓位) |
| AI 分析详情弹窗 | 完整的 4D-XV 信号表、红线审查、纪律评分、情景预案、经验教训 |
| 历史分析记录 | 展示该品种的历史 AI 分析记录列表,可点击查看完整报告 |
| 情景预案 | 突破/震荡/反转/消息影响四种情景的概率和应对策略 |
### 4.3 复盘计划V2
| 功能 | 描述 |
|------|------|
| 生成复盘报告 | 选择日期,调用 `POST /api/v1/review/generate` 自动生成 |
| 核心结论 | Hero 区域展示日期、核心结论、数据基准 |
| 三档分类 | 绿色(交易机会)/ 黄色(重点关注)/ 红色(规避) |
| 交易机会卡片 | 含方向、入场区间、止损、目标位、触发条件、四维评分条 |
| 全品种排名表 | 支持按综合评分/活跃度/量比/方向排序 |
| 品种详情弹窗 | 评分明细、多周期趋势、枢轴点位、交易计划 |
| 板块热度 | 各板块均分、趋势、龙头品种 |
| 风险提示 | 全局风险提示横幅 |
### 4.4 交易复盘
| 功能 | 描述 |
|------|------|
| 结算单导入 | 支持单个/批量导入 .xls/.xlsx 期货结算单 |
| 去重校验 | 按交易日+品种+时间+价格逐条去重 |
| 6 维统计卡片 | 总盈亏、净盈亏、胜率/盈亏比、交易笔数、最大回撤、日均盈亏 |
| 账户权益曲线 | ECharts 折线+柱状图(累计净盈亏 + 每日盈亏) |
| 品种盈亏气泡图 | 气泡大小=手续费占比,颜色=净盈亏 |
| 每日交易详情表 | 点击日期查看详情弹窗(含 AI 诊断报告、品种盈亏统计、交易流水) |
| 品种盈亏排行表 | 含盈亏比异步计算 |
| 品种详情弹窗 | K 线走势(多周期切换)+ 该品种交易记录 |
| 交易详情弹窗 | 单笔交易详情 + K 线买卖点标注 + AI 交易分析 |
| 批次管理 | 查看/删除导入批次 |
| 按日期删除 | 删除指定日期的所有交易记录 |
---
## 五、数据模型14 个 ORM 类)
| 模型类 | 表名 | 用途 |
|--------|------|------|
| `FuturesAnalysis` | `futures_analysis` | 期货分析报告(建议、指标、评分、点位) |
| `WatchedSymbol` | `watched_symbols` | 用户关注品种 |
| `AIModelConfig` | `ai_model_configs` | AI 模型配置(提供商、密钥、参数) |
| `AnalysisSettings` | `analysis_settings` | 分析设置(单例 JSON 配置) |
| `AIAnalysisCache` | `ai_analysis_cache` | AI 分析结果缓存(含 K 线时间戳用于智能刷新) |
| `ReviewDate` | `review_dates` | 复盘日期V1 |
| `SymbolRanking` | `symbol_rankings` | 品种排名V1 |
| `TradingPlan` | `trading_plans` | 交易计划V1 |
| `SymbolScoreV2` | `symbol_scores_v2` | V2 品种多维度评分5维度+综合+关键点位+方向标签) |
| `TradingPlanV2` | `trading_plans_v2` | V2 交易计划(入场/止损/目标/触发条件) |
| `SectorHeat` | `sector_heat` | 板块热度(均分/趋势/龙头/成员) |
| `ReviewPlanV2` | `review_plans_v2` | V2 复盘计划总表(元数据+统计) |
| `TradeRecord` | `trade_records` | 交易记录(从结算单导入的逐笔明细) |
| `TradeImportBatch` | `trade_import_batches` | 导入批次元信息 |
---
## 六、组件连接关系
```
+-------------------+
| 前端 SPA 页面 |
| futures_analysis |
| .html / .js / .css|
+---------+---------+
|
+-------------------+-------------------+
| | |
/api/v1/futures/* /api/v1/review/* /api/v1/trade-review/*
| | |
+---------+--------+ +------+-------+ +--------+--------+
|futures_analysis.py| |review_plan.py| | trade_review.py |
+----+------+------+ +------+-------+ +--------+--------+
| | | |
| +-------+--------+----------+--------+
| | |
+----+----+ +------+-------+ +--------+--------+
|cache.py | |plan_generator| | trade_parser |
+----+----+ +--------------+ +-----------------+
|
+----+----+ +------------------+
|collector| --> | futures_data_ |
| .py | | collector(脚本) |
+---------+ +------------------+
|
+----+----+ +------------------+
| storage | --> | Redis / MySQL / |
| Manager | | SQLite(兜底) |
+---------+ +------------------+
|
+----+----+
|config | --> symbols_config.json / ai_config.json / MySQL AppConfig
|_store |
+---------+
|
+----+-----------+
|ai_analysis.py | --> 外部 AI 模型 API (OpenAI 兼容)
|(AIFuturesAnalyzer)
+----------------+
```
---
## 七、存储架构
采用**三级降级策略**
1. **MySQL 优先** — 生产环境主存储
2. **Redis / StorageManager** — 缓存层加速
3. **SQLite 兜底** — 开发/单机环境自动降级
`analysis_db.py` 中的 `get_analysis_db()` 动态检测 MySQL 可用性,透明切换后端,业务代码无需感知底层存储变化。
---
## 八、路由注册
`app/main.py` 中注册三个路由:
```python
# 第 179-182 行
app.include_router(futures_analysis.router) # /api/v1/futures
app.include_router(review_plan.router) # /api/v1/review
app.include_router(trade_review.router) # /api/v1/trade-review
```
页面入口:`GET /futures-analysis`
---
## 九、技术指标计算说明
### MACDMoving Average Convergence Divergence
- EMA12 = 前一日EMA12 × (1 - 2/13) + 今日收盘价 × 2/13
- EMA26 = 前一日EMA26 × (1 - 2/27) + 今日收盘价 × 2/27
- DIF = EMA12 - EMA26
- DEA = 前一日DEA × (1 - 2/10) + 今日DIF × 2/10
- 信号DIF > DEA → 金叉看多DIF < DEA
### RSIRelative Strength Index
- 14 周期 RSI = 100 - 100 / (1 + RS)RS = 平均上涨幅度 / 平均下跌幅度
- 信号RSI > 70 → 超买RSI < 30 30 RSI 70
### 布林带Bollinger Bands
- 中轨 = 20 周期 MA
- 上轨 = 中轨 + 2 × 标准差
- 下轨 = 中轨 - 2 × 标准差
- 信号:价格触及上轨 → 压力;价格触及下轨 → 支撑
### KDJ随机指标
- RSV = (收盘价 - 9日最低价) / (9日最高价 - 9日最低价) × 100
- K = 2/3 × 前一日K + 1/3 × RSV
- D = 2/3 × 前一日D + 1/3 × K
- 信号K > D → 偏多K < D
### Pivot Point枢轴点
- PP = (最高价 + 最低价 + 收盘价) / 3
- R1 = 2 × PP - 最低价
- R2 = PP + (最高价 - 最低价)
- S1 = 2 × PP - 最高价
- S2 = PP - (最高价 - 最低价)

@ -0,0 +1,575 @@
---
comet_change: trade-reflection-system
role: technical-design
canonical_spec: openspec
archived-with: trade-reflection-system
---
# 交易反思系统 (Trade Reflection System) - Design Doc
## 1. 概述
在现有交易复盘模块基础上,构建「人工反思 → AI 提炼 → 经验沉淀」的学习闭环。核心能力包括按天组织交易视图、跨日交易自动配对、结构化反思、标签系统、AI 重新分析、历史经验库。
## 2. 架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 前 端 层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 按天交易视图 │ │ 反思编辑面板 │ │ 经验库页面 │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ └─────────────────┴────────────────────┘ │
│ 现有交易复盘页面 (改造) │
└─────────────────────────┬───────────────────────────────────┘
│ REST API
┌─────────────────────────┴───────────────────────────────────┐
│ 后 端 层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ trade_review.py (现有 + 8个新端点) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │trade_parser │ │ ai_analysis.py (扩展) │ │
│ │(不变) │ │ └── 反思增强分析 + 版本存储 │ │
│ └──────────────┘ └──────────────────────────────┘ │
└─────────────────────────┬───────────────────────────────────┘
│ SQLAlchemy ORM
┌─────────────────────────┴───────────────────────────────────┐
│ 数 据 层 │
│ TradeRecord(扩展) + 6新表 + SQLite/MySQL │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 模块职责
| 模块 | 职责 | 变更类型 |
|------|------|---------|
| `trade_review.py` | 新增 8 个 API 端点 | 扩展 |
| `analysis_models.py` | 扩展 TradeRecord + 新增 6 个模型 | 扩展 |
| `ai_analysis.py` | 新增反思增强分析提示词 + 版本化存储 | 扩展 |
| `trade_parser.py` | 不变 | 无变更 |
| `futures_analysis.js` | 新增前端视图和交互逻辑 | 扩展 |
## 3. 数据模型详细设计
### 3.1 TradeRecord (扩展)
在现有表上新增字段:
```python
# 新增字段
reflection_status = Column(String(16), default='none') # none/pending/done
```
- `none`: 未写反思
- `pending`: 已写反思,待 AI 分析
- `done`: 已完成 AI 分析
### 3.2 TradePair (新表)
存储跨日交易配对关系:
```python
class TradePair(AnalysisBase):
__tablename__ = "trade_pairs"
id = Column(Integer, primary_key=True, autoincrement=True)
pair_type = Column(String(16), nullable=False) # auto/manual
symbol = Column(String(32), nullable=False, index=True)
variety = Column(String(16), nullable=False, index=True)
direction = Column(String(8), nullable=False) # long/short
open_trade_id = Column(Integer, nullable=False, index=True)
open_date = Column(String(16), nullable=False)
open_time = Column(String(32), nullable=False)
open_price = Column(Float, nullable=True)
close_trade_ids = Column(JSON, nullable=True) # [id1, id2, ...]
close_date = Column(String(16), nullable=True)
close_time = Column(String(32), nullable=True)
close_price = Column(Float, nullable=True)
status = Column(String(16), nullable=False, default='open') # open/closed
pnl = Column(Float, nullable=True) # 配对盈亏
commission_total = Column(Float, nullable=True) # 总手续费
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
```
### 3.3 TradeReflection (新表)
每笔交易的结构化反思:
```python
class TradeReflection(AnalysisBase):
__tablename__ = "trade_reflections"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_id = Column(Integer, nullable=False, index=True)
trade_pair_id = Column(Integer, nullable=True, index=True)
# 模板字段
entry_reason = Column(Text, nullable=True) # 入场理由
entry_timing = Column(String(16), nullable=True) # good/fair/poor
position_management = Column(Text, nullable=True) # 仓位管理反思
exit_reason = Column(Text, nullable=True) # 出场理由
exit_timing = Column(String(16), nullable=True) # good/fair/poor
discipline_score = Column(Integer, nullable=True) # 1-5
# 自由文本
free_reflection = Column(Text, nullable=True)
# AI 分析状态
ai_analysis_status = Column(String(16), default='none') # none/pending/done
ai_analysis_version = Column(Integer, default=0)
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
```
### 3.4 DailyReflection (新表)
当日整体反思:
```python
class DailyReflection(AnalysisBase):
__tablename__ = "daily_reflections"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_date = Column(String(16), nullable=False, unique=True, index=True)
emotion_state = Column(String(32), nullable=True) # 情绪状态
market_judgment = Column(Text, nullable=True) # 市场判断
execution_discipline = Column(Integer, nullable=True) # 1-5
overall_rating = Column(Integer, nullable=True) # 1-5
summary = Column(Text, nullable=True) # 总结
improvements = Column(Text, nullable=True) # 改进方向
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
```
### 3.5 TradeTag (新表)
标签定义:
```python
class TradeTag(AnalysisBase):
__tablename__ = "trade_tags"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(64), nullable=False, unique=True)
category = Column(String(32), nullable=True) # operation/mindset/discipline/technical/custom
description = Column(Text, nullable=True)
is_preset = Column(Boolean, nullable=False, default=False)
usage_count = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=datetime.now)
```
预设标签清单:
- 操作类: 追涨杀跌, 逆市操作, 提前入场, 延迟出场, 加仓过早
- 心态类: 情绪化交易, 恐惧平仓, 贪婪持仓, 报复性交易
- 纪律类: 严格执行计划, 违反止损, 超仓交易, 频繁交易
- 技术类: 突破交易, 回调交易, 趋势跟踪, 震荡交易
### 3.6 TradeRecordTag (关联表)
```python
class TradeRecordTag(AnalysisBase):
__tablename__ = "trade_record_tags"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_id = Column(Integer, nullable=False, index=True)
tag_id = Column(Integer, nullable=False, index=True)
__table_args__ = (
Index('ix_trade_tag_unique', 'trade_id', 'tag_id', unique=True),
)
```
### 3.7 TradeExperience (新表)
经验教训:
```python
class TradeExperience(AnalysisBase):
__tablename__ = "trade_experiences"
id = Column(Integer, primary_key=True, autoincrement=True)
trade_id = Column(Integer, nullable=False, index=True)
trade_pair_id = Column(Integer, nullable=True, index=True)
reflection_id = Column(Integer, nullable=True, index=True)
title = Column(String(128), nullable=False) # 经验标题
content = Column(Text, nullable=False) # 经验内容
lesson_type = Column(String(16), nullable=False) # lesson/tip/warning
tags = Column(JSON, nullable=True) # [tag_name, ...]
ai_version = Column(Integer, nullable=True) # AI分析版本号
created_from = Column(String(16), nullable=False) # ai/manual
created_at = Column(DateTime, nullable=False, default=datetime.now)
updated_at = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
```
## 4. API 详细设计
### 4.1 按天获取交易+配对
```
GET /api/v1/trade-review/daily-trades/{date}
Response:
{
"success": true,
"data": {
"trade_date": "2026-07-05",
"trades": [...], // 原始交易记录
"pairs": [...], // 配对结果
"daily_reflection": {...}, // 当日反思 (如有)
"statistics": {...} // 当天统计
}
}
```
### 4.2 配对 CRUD
```
POST /api/v1/trade-review/trade-pairs
{
"pair_type": "manual",
"symbol": "AG2608",
"open_trade_id": 123,
"close_trade_ids": [456, 789]
}
PUT /api/v1/trade-review/trade-pairs/{id}
DELETE /api/v1/trade-review/trade-pairs/{id}
```
### 4.3 反思 CRUD
```
POST /api/v1/trade-review/reflections
{
"trade_id": 123,
"entry_reason": "...",
"entry_timing": "good",
"position_management": "...",
"exit_reason": "...",
"exit_timing": "fair",
"discipline_score": 4,
"free_reflection": "..."
}
PUT /api/v1/trade-review/reflections/{id}
GET /api/v1/trade-review/reflections?trade_id=123
```
### 4.4 当日反思 CRUD
```
POST /api/v1/trade-review/daily-reflections
{
"trade_date": "2026-07-05",
"emotion_state": "平静",
"market_judgment": "...",
"execution_discipline": 4,
"overall_rating": 4,
"summary": "...",
"improvements": "..."
}
PUT /api/v1/trade-review/daily-reflections/{id}
GET /api/v1/trade-review/daily-reflections/{date}
```
### 4.5 标签管理
```
GET /api/v1/trade-review/tags # 获取所有标签
POST /api/v1/trade-review/tags # 创建自定义标签
{
"name": "我的标签",
"category": "custom"
}
POST /api/v1/trade-review/records/{trade_id}/tags
{
"tag_ids": [1, 2, 3]
}
DELETE /api/v1/trade-review/records/{trade_id}/tags/{tag_id}
```
### 4.6 AI 重新分析
```
POST /api/v1/trade-review/reanalyze
{
"trade_id": 123, # 或 trade_pair_id
"reflection_id": 456
}
Response:
{
"success": true,
"data": {
"analysis_id": 789,
"version": 2,
"analysis": "...",
"experience_suggestions": [...] # 经验提炼建议
}
}
```
### 4.7 经验库 CRUD
```
GET /api/v1/trade-review/experiences
?tag=追涨杀跌
&lesson_type=warning
&keyword=止损
&page=1&page_size=20
POST /api/v1/trade-review/experiences
{
"trade_id": 123,
"title": "...",
"content": "...",
"lesson_type": "lesson",
"tags": ["追涨杀跌", "违反止损"]
}
PUT /api/v1/trade-review/experiences/{id}
DELETE /api/v1/trade-review/experiences/{id}
```
## 5. 核心业务流程
### 5.1 交易配对流程
```
1. 用户导入结算单 → TradeRecord 表
2. 用户选择日期 → 触发配对逻辑
3. 配对引擎:
a. 按 variety + trade_date 筛选
b. 按品种分组
c. 对每个品种:
- 找出所有开仓记录 (offset='开')
- 找出所有平仓记录 (offset='平')
- 按时间顺序配对 (FIFO)
- 同一方向连续开仓合并为一个头寸
d. 生成 TradePair 记录 (pair_type='auto')
4. 前端展示配对结果
5. 用户可手动修正 (创建 pair_type='manual' 记录)
```
### 5.2 反思 → AI 分析 → 经验提炼流程
```
1. 用户给某笔交易写反思 → POST /reflections
2. 系统更新 TradeRecord.reflection_status = 'pending'
3. 用户点击「重新 AI 分析」→ POST /reanalyze
4. 后端:
a. 获取交易详情 + 多周期 K 线
b. 获取反思内容
c. 构建增强提示词 (交易信息 + K 线 + 反思)
d. 调用 AI 模型
e. 解析响应,保存为 TradeExperience (ai_version=N)
f. 更新 TradeReflection.ai_analysis_status = 'done'
g. 更新 TradeReflection.ai_analysis_version = N
h. 更新 TradeRecord.reflection_status = 'done'
5. 前端展示 AI 分析结果和经验建议
6. 用户可编辑经验建议后保存
```
### 5.3 AI 提示词设计
```python
prompt = f"""
你是一个有20年经验的期货交易教练。请分析以下交易及其反思提炼经验教训。
【交易信息】
品种: {symbol}
方向: {direction}
开仓: {open_date} {open_time} @ {open_price}
平仓: {close_date} {close_time} @ {close_price}
盈亏: {pnl}
手续费: {commission}
【多周期 K 线 context】
{kline_context}
【交易者反思】
入场理由: {entry_reason}
入场时机评价: {entry_timing}
仓位管理反思: {position_management}
出场理由: {exit_reason}
出场时机评价: {exit_timing}
纪律评分: {discipline_score}/5
自由反思: {free_reflection}
请从以下维度分析并输出 JSON:
{{
"trade_analysis": "对这笔交易的综合评价",
"strengths": ["优点1", "优点2"],
"weaknesses": ["不足1", "不足2"],
"experience_lessons": [
{{
"title": "经验标题",
"content": "经验内容",
"lesson_type": "lesson/tip/warning",
"tags": ["标签1", "标签2"]
}}
]
}}
"""
```
## 6. 前端设计
### 6.1 按天交易视图
```
┌─────────────────────────────────────────────────────────────┐
│ 日期选择器: [2026-07-05] [<] [>] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 当日反思 [编辑] │ │
│ │ 情绪: 平静 | 纪律: ★★★★ | 总体: ★★★★ │ │
│ │ 总结: ... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 交易配对 #1 (AG2608 多头) 盈亏: +1200 │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 开 09:30│→ │ 平 14:20│ [反思] [标签] [AI分析] │ │
│ │ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 交易配对 #2 (RB2610 空头) 盈亏: -800 │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 开 10:15│→ │ 平 11:30│ [反思✓] [标签] [AI分析✓] │ │
│ │ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 未配对交易: CU2609 买开 09:45 @ 78500 │ │
│ │ [手动配对] [反思] [标签] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 6.2 反思编辑面板 (Modal)
```
┌─────────────────────────────────────────────────────────────┐
│ 反思编辑 - AG2608 多头 09:30→14:20 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 入场理由: [文本框] │
│ 入场时机: ○ 良好 ○ 一般 ○ 较差 │
│ │
│ 仓位管理反思: [文本框] │
│ │
│ 出场理由: [文本框] │
│ 出场时机: ○ 良好 ○ 一般 ○ 较差 │
│ │
│ 纪律评分: ☆☆☆☆☆ │
│ │
│ ─── 自由反思 ─── │
│ [多行文本框] │
│ │
│ ─── 标签 ─── │
│ [预设标签选择] + [自定义标签输入] │
│ │
│ [保存] [保存并AI分析] [取消] │
└─────────────────────────────────────────────────────────────┘
```
### 6.3 经验库页面
```
┌─────────────────────────────────────────────────────────────┐
│ 历史经验库 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [搜索框] [标签筛选: 全部▼] [类型: 全部▼] [视图: 列表▼] │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 2026-07-05 | ⚠️ 警告 │ │
│ │ 追涨杀跌导致亏损 - AG2608 │ │
│ │ 在突破不明显的情况下追多,未设止损... │ │
│ │ 标签: 追涨杀跌 | 违反止损 │ │
│ │ [查看原交易] [编辑] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 2026-07-04 | 💡 经验 │ │
│ │ 严格执行计划带来稳定收益 - RB2610 │ │
│ │ 按计划入场,按止损出场,避免情绪干扰... │ │
│ │ 标签: 严格执行计划 | 趋势跟踪 │ │
│ │ [查看原交易] [编辑] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [时间线视图] [分类视图] │
└─────────────────────────────────────────────────────────────┘
```
## 7. 错误处理
| 场景 | 处理策略 |
|------|---------|
| 配对逻辑无法匹配 | 标记为「未配对交易」,用户手动配对 |
| AI 模型调用失败 | 返回错误提示,保留 pending 状态,可重试 |
| 反思为空时触发 AI 分析 | 允许,但提示用户「补充反思可获得更精准的分析」 |
| 标签重复创建 | 自动去重,返回已存在标签 |
| 经验保存时交易已删除 | 软删除标记,经验保留但标记「来源已删除」 |
## 8. Demo 确认环节
在设计完成后、执行代码前,需要创建一个交互式 Demo 展示以下关键交互流程:
1. 按天查看交易 + 配对展示
2. 编辑反思 + 打标签
3. 触发 AI 重新分析
4. 经验提炼和保存
5. 经验库浏览和筛选
Demo 使用静态数据模拟,确认交互后再进入实际开发。
## 9. 测试策略
### 9.1 单元测试
- `test_trade_pairing.py`: 配对逻辑测试 (FIFO、跨日、多次开仓)
- `test_ai_prompt_builder.py`: AI 提示词构建测试
- `test_experience_extractor.py`: 经验提炼逻辑测试
### 9.2 API 测试
- `test_reflection_api.py`: 反思 CRUD
- `test_tag_api.py`: 标签管理
- `test_experience_api.py`: 经验库 CRUD
- `test_reanalyze_api.py`: AI 重新分析
### 9.3 集成测试
- `test_reflection_to_experience_flow.py`: 完整流程测试
### 9.4 边界测试
- 空反思 AI 分析
- 跨日配对边界 (周末/节假日)
- 标签去重
- 经验库分页/搜索/筛选
## 10. 非目标确认
- 不改变现有结算单导入逻辑
- 不改变 K 线数据获取和展示
- 不改变复盘计划 V2 生成逻辑
- 不改变品种分析主页
- 不重构现有交易复盘统计功能

@ -0,0 +1,15 @@
name: trade-reflection-system
phase: archive
workflow: full
created_at: "2026-07-05T22:21:00+08:00"
updated_at: "2026-07-05T22:40:00+08:00"
context_compression: off
handoff_context: openspec/changes/trade-reflection-system/.comet/handoff/design-context.md
design_doc: docs/superpowers/specs/2026-07-05-trade-reflection-system-design.md
plan: null
build_mode: direct
isolation: branch
tdd_mode: direct
build_pause: null
verify_result: pass
archived: true

@ -0,0 +1,38 @@
# Brainstorm Summary
- Change: trade-reflection-system
- Date: 2026-07-05
## 确认的技术方案
### 架构
- 前端: 新增按天交易视图、反思编辑面板、经验库页面;改造现有交易复盘页面
- 后端: 扩展 trade_review.py 新增 8 个 API 端点
- 数据: 扩展 TradeRecord 表 + 新增 6 个表 (TradePair, TradeReflection, DailyReflection, TradeTag, TradeRecordTag, TradeExperience)
### 关键决策
- 交易配对: 持久化存储 (TradePair 表),支持手动修正
- AI 分析: 版本化存储,可对比反思前后差异
- 经验教训: 关联到具体交易,保留完整上下文
- 反思形式: 模板 (入场理由/时机评价/仓位管理/出场理由/纪律评分) + 自由文本
- 标签系统: 预设 + 自定义,分类管理
- 经验库: 列表+搜索+标签筛选+分类视图+时间线视图组合
- AI 重新分析: 单笔触发,只分析已写反思的交易
- 执行流程: 设计完成后先做 Demo 确认交互再执行
## 关键取舍与风险
- 持久化配对 vs 虚拟配对: 选择持久化以支持手动修正和查询性能,代价是需要同步维护配对表
- 版本化 AI 分析 vs 覆盖: 选择版本化以支持对比,代价是存储成本和 UI 复杂度
- 经验关联交易 vs 独立: 选择关联以保留上下文,代价是经验库查询需要 join
## 测试策略
- 单元测试: 配对逻辑、AI 提示词构建、经验提炼逻辑
- API 测试: 所有新增端点的 CRUD 操作
- 集成测试: 反思 → AI 分析 → 经验保存完整流程
- 边界测试: 跨日配对边界、空反思分析、标签去重
## Spec Patch
无 (所有能力均为新增,不修改既有 spec)

@ -0,0 +1,33 @@
# OpenSpec Handoff Context: trade-reflection-system
## Source
- proposal.md: openspec/changes/trade-reflection-system/proposal.md
## Why
当前交易复盘模块只提供事后统计能力缺少人工反思→AI提炼→经验沉淀的学习闭环。需要构建完整的交易反思系统。
## Capabilities (New)
- trade-reflection: 每笔交易的结构化反思(模板+自由文本)和自由打标签
- daily-reflection: 当日交易整体反思(多维度总结)
- trade-pairing: 跨日交易自动配对(按品种+方向+时间段),支持手动修正
- ai-reanalysis: 基于反思内容的AI重新分析提炼经验教训
- experience-library: 历史经验库,支持搜索、标签筛选、分类视图、时间线视图
- trade-tagging: 交易标签系统(预设+自定义标签管理)
## Capabilities (Modified)
- trade-review: 交易记录展示从纯统计视图扩展为支持反思、标签、AI重新分析的交互视图
## Impact
- 受影响代码: app/api/trade_review.py, app/analysis_models.py, app/static/futures_analysis.js
- 数据库变更: TradeRecord表扩展 + 新增4-5个表
- 不影响: 结算单解析、K线数据获取、复盘计划生成、品种分析主页
## Key Decisions (from exploration)
- 跨日交易: 自动配对+手动补充(按品种+方向+时间段)
- 反思形式: 模板+自由文本
- 当日反思: 独立反思+维度选择
- 经验提炼: AI辅助+人工编辑
- 标签系统: 预设+自定义
- 经验库展示: 列表+搜索+标签筛选+分类视图+时间线视图(组合使用)
- AI重新分析: 只重新分析已写反思的交易(单笔触发)
- 执行前需做Demo确认交互

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-05

@ -0,0 +1,33 @@
## Why
当前交易复盘模块只提供「事后统计」能力盈亏曲线、品种排行、AI 逐笔分析),但缺少**人工反思 → AI 提炼 → 经验沉淀**的学习闭环。交易者无法对每笔交易进行结构化反思,也无法将经验教训系统化积累和复用。现在需要在现有功能基础上构建完整的交易反思系统,让复盘从「看数据」升级为「学经验」。
## What Changes
- 扩展 `TradeRecord` 数据模型增加反思、标签、AI 分析结果字段
- 新增「按天组织交易」视图,支持跨日交易自动配对(按品种+方向+时间段)
- 新增每笔交易的反思编辑面板(模板+自由文本),支持打标签(预设+自定义)
- 新增当日整体反思功能(多维度总结:情绪、纪律、市场判断等)
- 新增 AI 经验提炼能力:基于反思内容重新分析,提炼经验教训并支持人工编辑后保存到经验库
- 新增历史经验库模块:列表+搜索+标签筛选+分类视图+时间线视图
- 新增交互 Demo 确认环节(设计完成后先做 Demo 再执行)
## Capabilities
### New Capabilities
- `trade-reflection`: 每笔交易的结构化反思(模板+自由文本)和自由打标签
- `daily-reflection`: 当日交易整体反思(多维度总结)
- `trade-pairing`: 跨日交易自动配对(按品种+方向+时间段),支持手动修正
- `ai-reanalysis`: 基于反思内容的 AI 重新分析,提炼经验教训
- `experience-library`: 历史经验库,支持搜索、标签筛选、分类视图、时间线视图
- `trade-tagging`: 交易标签系统(预设+自定义标签管理)
### Modified Capabilities
- `trade-review`: 交易记录展示从纯统计视图扩展为支持反思、标签、AI 重新分析的交互视图
## Impact
- **受影响代码**`app/api/trade_review.py`、`app/analysis_models.py`、`app/static/futures_analysis.js`(交易复盘相关部分)
- **新增代码**:反思 API、标签 API、经验库 API、前端反思面板、经验库页面
- **数据库变更**`TradeRecord` 表扩展 + 新增 4-5 个表(交易反思、当日反思、经验教训、标签、标签关联)
- **不影响**结算单解析逻辑、K 线数据获取、复盘计划生成、品种分析主页

@ -0,0 +1,35 @@
# ai-reanalysis spec
## ADDED Requirements
### Requirement: 基于反思的 AI 重新分析
系统 SHALL 允许用户对已写反思的交易触发 AI 重新分析AI 将结合交易信息、多周期 K 线和反思内容生成综合评价和经验提炼建议。
#### Scenario: 用户触发 AI 重新分析
- **WHEN** 用户为一笔交易写完反思
- **AND** 用户点击「重新 AI 分析」
- **THEN** 系统获取交易详情和多周期 K 线
- **AND** 构建增强提示词(交易信息 + K 线 + 反思)
- **AND** 调用 AI 模型
- **AND** 解析响应并保存为 TradeExperience
- **AND** 更新反思的 ai_analysis_status 为 'done'
- **AND** 更新 ai_analysis_version
### Requirement: 版本化 AI 分析
系统 SHALL 保留每次 AI 分析的历史版本,用户可查看任意版本的分析结果。
#### Scenario: 多次 AI 分析
- **WHEN** 用户第一次触发 AI 分析
- **THEN** ai_analysis_version = 1
- **WHEN** 用户修改反思后再次触发
- **THEN** ai_analysis_version = 2
- **AND** 版本 1 和版本 2 均保留
### Requirement: 经验提炼建议
系统 SHALL 在 AI 分析结果中包含经验提炼建议,用户可编辑后保存为正式经验教训。
#### Scenario: AI 生成经验建议
- **WHEN** AI 分析完成
- **THEN** 返回 experience_suggestions 列表
- **AND** 每个建议包含 title、content、lesson_type、tags
- **AND** 用户可编辑后保存为 TradeExperience

@ -0,0 +1,21 @@
# daily-reflection spec
## ADDED Requirements
### Requirement: 每日可添加整体反思
系统 SHALL 允许用户为每个交易日添加整体反思,包括情绪状态、市场判断、执行纪律评分、总体评分、总结、改进方向。
#### Scenario: 用户添加当日反思
- **WHEN** 用户在按天交易视图中点击「编辑当日反思」
- **AND** 用户填写各维度字段
- **AND** 用户点击保存
- **THEN** 系统保存当日反思记录
### Requirement: 每日反思唯一
系统 SHALL 保证每个交易日只有一条反思记录,重复编辑时更新已有记录。
#### Scenario: 用户修改当日反思
- **WHEN** 用户再次打开已有反思的日期
- **AND** 用户修改任意字段
- **AND** 用户点击保存
- **THEN** 系统更新该日期的反思记录

@ -0,0 +1,32 @@
# experience-library spec
## ADDED Requirements
### Requirement: 经验教训存储
系统 SHALL 允许保存经验教训记录,每条经验关联到来源交易,包含标题、内容、类型、标签。
#### Scenario: 保存 AI 提炼的经验
- **WHEN** AI 分析完成并返回经验建议
- **AND** 用户编辑并确认保存
- **THEN** 系统创建 TradeExperience 记录
- **AND** 关联到来源交易和反思
- **AND** 标记 created_from='ai'
### Requirement: 经验库浏览
系统 SHALL 提供经验库浏览功能,支持列表视图、搜索、标签筛选、类型筛选、分页。
#### Scenario: 按标签筛选经验
- **WHEN** 用户在经验库中选择标签「追涨杀跌」
- **THEN** 系统返回所有包含该标签的经验记录
### Requirement: 多视图展示
系统 SHALL 支持列表视图、分类视图(按标签/主题分组)、时间线视图(按时间轴展示)。
### Requirement: 经验可编辑
系统 SHALL 允许用户编辑已保存的经验教训。
#### Scenario: 用户修改经验内容
- **WHEN** 用户打开某条经验
- **AND** 用户修改内容或标签
- **AND** 用户点击保存
- **THEN** 系统更新该经验记录

@ -0,0 +1,34 @@
# trade-pairing spec
## ADDED Requirements
### Requirement: 自动交易配对
系统 SHALL 在用户选择日期后,自动按品种+方向+时间段配对开平仓记录。
#### Scenario: 自动配对成功
- **WHEN** 用户选择某个交易日
- **AND** 系统检测到该日期有交易记录
- **THEN** 系统执行自动配对逻辑
- **AND** 生成 pair_type='auto' 的 TradePair 记录
- **AND** 展示配对结果给前端
### Requirement: FIFO 配对规则
系统 SHALL 按时间顺序FIFO将开仓记录与平仓记录配对。同一品种同一方向的连续开仓合并为一个头寸。
#### Scenario: 多次开仓配对
- **WHEN** 某品种有 2 笔开仓和 3 笔平仓
- **THEN** 系统将 2 笔开仓合并为一个头寸
- **AND** 按时间顺序匹配 3 笔平仓
### Requirement: 手动配对修正
系统 SHALL 允许用户手动创建、修改、删除配对关系。
#### Scenario: 用户手动修正配对
- **WHEN** 用户对自动配对结果不满意
- **AND** 用户选择开仓记录和平仓记录
- **AND** 用户点击「手动配对」
- **THEN** 系统创建 pair_type='manual' 的 TradePair 记录
- **AND** 移除相关的 auto 配对记录
### Requirement: 未配对交易展示
系统 SHALL 将无法配对的单边交易单独展示,并提供手动配对入口。

@ -0,0 +1,26 @@
# trade-reflection spec
## ADDED Requirements
### Requirement: 每笔交易可添加结构化反思
系统 SHALL 允许用户为每笔交易记录添加结构化反思,包括入场理由、入场时机评价、仓位管理反思、出场理由、出场时机评价、纪律评分。
#### Scenario: 用户为交易添加完整反思
- **WHEN** 用户选择一笔交易并打开反思编辑面板
- **AND** 用户填写所有模板字段和自由反思
- **AND** 用户点击保存
- **THEN** 系统保存反思记录
- **AND** 更新该交易的 reflection_status 为 'pending'
### Requirement: 反思可编辑和更新
系统 SHALL 允许用户编辑已保存的反思内容。
#### Scenario: 用户修改已有反思
- **WHEN** 用户打开已有反思的交易
- **AND** 用户修改任意字段
- **AND** 用户点击保存
- **THEN** 系统更新反思记录
- **AND** 重置 ai_analysis_status 为 'none'(需重新 AI 分析)
### Requirement: 反思状态追踪
系统 SHALL 追踪每笔交易的反思状态none未反思、pending待 AI 分析、done已完成 AI 分析)。

@ -0,0 +1,37 @@
# trade-tagging spec
## ADDED Requirements
### Requirement: 预设标签管理
系统 SHALL 内置预设标签,按操作类、心态类、纪律类、技术类分类。
#### Scenario: 查看预设标签
- **WHEN** 用户打开标签选择面板
- **THEN** 系统展示所有预设标签
- **AND** 按分类分组展示
### Requirement: 自定义标签创建
系统 SHALL 允许用户创建自定义标签。
#### Scenario: 用户创建新标签
- **WHEN** 用户输入自定义标签名称
- **AND** 该标签不存在
- **THEN** 系统创建 is_preset=false 的标签记录
### Requirement: 交易打标签
系统 SHALL 允许用户为任意交易记录添加/移除标签。
#### Scenario: 给交易添加标签
- **WHEN** 用户在反思面板中选择标签
- **AND** 用户点击保存
- **THEN** 系统在 TradeRecordTag 中创建关联记录
- **AND** 更新对应标签的 usage_count
### Requirement: 标签去重
系统 SHALL 保证标签名称唯一,重复创建时返回已有标签。
#### Scenario: 创建已存在的标签
- **WHEN** 用户尝试创建「追涨杀跌」
- **AND** 该标签已存在
- **THEN** 系统返回已有标签
- **AND** 不创建重复记录

@ -0,0 +1,120 @@
## 1. 数据模型与数据库迁移
- [x] 1.1 在 analysis_models.py 中扩展 TradeRecord 模型,新增 reflection_status 字段
- [x] 1.2 创建 TradePair 模型(交易配对表)
- [x] 1.3 创建 TradeReflection 模型(交易反思表)
- [x] 1.4 创建 DailyReflection 模型(当日反思表)
- [x] 1.5 创建 TradeTag 模型(标签定义表)
- [x] 1.6 创建 TradeRecordTag 模型(交易-标签关联表)
- [x] 1.7 创建 TradeExperience 模型(经验教训表)
- [x] 1.8 运行数据库迁移,验证所有表结构正确创建
## 2. 交易配对引擎
- [x] 2.1 实现自动配对逻辑(按品种+方向+时间段FIFO 规则)
- [x] 2.2 实现连续开仓合并为一个头寸的逻辑
- [x] 2.3 实现手动创建配对 APIPOST /trade-pairs
- [x] 2.4 实现修改配对 APIPUT /trade-pairs/{id}
- [x] 2.5 实现删除配对 APIDELETE /trade-pairs/{id}
- [x] 2.6 实现配对盈亏计算逻辑
- [x] 2.7 编写配对逻辑单元测试FIFO、跨日、多次开仓场景
## 3. 反思 API
- [x] 3.1 实现创建反思 APIPOST /reflections
- [x] 3.2 实现更新反思 APIPUT /reflections/{id}
- [x] 3.3 实现查询反思 APIGET /reflections?trade_id=
- [x] 3.4 实现创建当日反思 APIPOST /daily-reflections
- [x] 3.5 实现更新当日反思 APIPUT /daily-reflections/{id}
- [x] 3.6 实现查询当日反思 APIGET /daily-reflections/{date}
- [x] 3.7 实现按天获取交易+配对+反思状态 APIGET /daily-trades/{date}
## 4. 标签系统 API
- [x] 4.1 实现获取所有标签 APIGET /tags含预设标签初始化
- [x] 4.2 实现创建自定义标签 APIPOST /tags含去重逻辑
- [x] 4.3 实现交易打标签 APIPOST /records/{id}/tags
- [x] 4.4 实现移除标签 APIDELETE /records/{id}/tags/{tag_id}
- [x] 4.5 编写标签去重和关联单元测试
## 5. AI 重新分析
- [x] 5.1 扩展 ai_analysis.py新增反思增强分析提示词构建函数
- [x] 5.2 实现 AI 重新分析 APIPOST /reanalyze
- [x] 5.3 实现多版本 AI 分析结果存储逻辑
- [x] 5.4 实现经验提炼建议解析和保存
- [x] 5.5 编写 AI 提示词构建单元测试
## 6. 经验库 API
- [x] 6.1 实现获取经验列表 APIGET /experiences支持标签/类型/关键词筛选和分页
- [x] 6.2 实现创建经验 APIPOST /experiences
- [x] 6.3 实现更新经验 APIPUT /experiences/{id}
- [x] 6.4 实现删除经验 APIDELETE /experiences/{id}
- [x] 6.5 实现按标签统计经验数量 API
## 7. 前端 - 按天交易视图
- [x] 7.1 创建日期选择器组件,支持前后切换日期
- [x] 7.2 实现按天获取交易+配对数据的前端调用
- [x] 7.3 实现交易配对卡片展示(开仓→平仓箭头样式)
- [x] 7.4 实现未配对交易单独展示
- [x] 7.5 实现手动配对交互(选择开仓+平仓→确认配对)
- [x] 7.6 实现当天统计卡片(总盈亏、交易笔数、胜率等)
## 8. 前端 - 反思编辑面板
- [x] 8.1 创建反思编辑 Modal 组件
- [x] 8.2 实现模板字段表单(入场理由、时机评价、仓位管理、出场理由、纪律评分)
- [x] 8.3 实现自由反思多行文本框
- [x] 8.4 实现反思保存 API 调用
- [x] 8.5 实现「保存并 AI 分析」按钮
- [x] 8.6 实现反思状态指示器(未反思/待分析/已完成)
## 9. 前端 - 标签系统
- [x] 9.1 创建标签选择组件(预设标签按分类展示)
- [x] 9.2 实现自定义标签输入框
- [x] 9.3 实现交易打标签交互
- [x] 9.4 实现已选标签展示和移除
## 10. 前端 - AI 分析结果展示
- [x] 10.1 创建 AI 分析结果展示面板
- [x] 10.2 实现多版本分析结果切换
- [x] 10.3 实现经验提炼建议展示和编辑
- [x] 10.4 实现经验保存交互
## 11. 前端 - 当日反思
- [x] 11.1 创建当日反思编辑区域
- [x] 11.2 实现多维度表单(情绪状态、市场判断、纪律评分、总体评分、总结、改进方向)
- [x] 11.3 实现当日反思保存和展示
## 12. 前端 - 经验库页面
- [x] 12.1 创建经验库独立页面/Tab
- [x] 12.2 实现经验列表视图(卡片样式)
- [x] 12.3 实现搜索框和筛选栏(标签筛选、类型筛选)
- [x] 12.4 实现分页加载
- [x] 12.5 实现经验详情弹窗(含来源交易链接)
- [x] 12.6 实现时间线视图切换
- [x] 12.7 实现分类视图切换(按标签分组)
## 13. Demo 交互确认
- [x] 13.1 创建 Demo 页面(使用静态模拟数据)
- [x] 13.2 实现按天查看交易 + 配对展示 Demo
- [x] 13.3 实现编辑反思 + 打标签 Demo
- [x] 13.4 实现触发 AI 重新分析 Demo模拟响应
- [x] 13.5 实现经验提炼和保存 Demo
- [x] 13.6 实现经验库浏览和筛选 Demo
- [x] 13.7 用户确认 Demo 交互流程
## 14. 集成测试与边界测试
- [x] 14.1 编写反思→AI 分析→经验保存完整流程集成测试
- [x] 14.2 编写跨日配对边界测试(周末/节假日)
- [x] 14.3 编写空反思 AI 分析测试
- [x] 14.4 编写经验库分页/搜索/筛选测试
- [x] 14.5 运行所有测试并修复失败项

@ -0,0 +1,287 @@
"""
交易反思系统单元测试与集成测试
"""
import json
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.analysis_db import AnalysisBase
from app.analysis_models import (
TradeRecord, TradePair, TradeReflection, DailyReflection,
TradeTag, TradeRecordTag, TradeExperience, TradeAIAnalysis,
)
from app.services.trade_pairing_engine import (
auto_pair_trades,
create_manual_pair,
delete_pair,
get_daily_trades_with_pairs,
)
from app.services.reflection_ai_analysis import (
build_reflection_prompt,
_parse_ai_response,
save_suggestion_as_experience,
)
@pytest.fixture
def db():
"""提供已创建表结构的内存 SQLite 会话analysis 库)。"""
engine = create_engine("sqlite:///:memory:")
AnalysisBase.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
def _make_record(db, symbol="AG2608", variety="AG", direction="", offset="",
price=7850, volume=2, close_pnl=0, commission=10,
trade_date="2026-07-05", trade_time="09:30:00", import_batch="test-batch"):
"""辅助函数:创建交易记录"""
r = TradeRecord(
trade_type="期货",
symbol=symbol,
variety=variety,
symbol_name="沪银",
direction=direction,
offset=offset,
price=price,
volume=volume,
close_pnl=close_pnl,
commission=commission,
trade_date=trade_date,
trade_time=trade_time,
import_batch=import_batch,
)
db.add(r)
db.flush()
return r
class TestTradePairingEngine:
"""交易配对引擎测试"""
def test_fifo_pairing_single_day(self, db):
"""单日 FIFO 配对:先开先平"""
open1 = _make_record(db, direction="", offset="", price=7850, volume=2, trade_time="09:30:00")
close1 = _make_record(db, direction="", offset="", price=7910, volume=2,
close_pnl=1200, commission=10, trade_time="14:20:00")
result = auto_pair_trades(db, "2026-07-05")
db.commit()
assert result["created"] == 1
pairs = db.query(TradePair).all()
assert len(pairs) == 1
assert pairs[0].direction == "long"
assert pairs[0].open_price == 7850
assert pairs[0].close_price == 7910
assert pairs[0].net_pnl == 1180 # 1200 - 10 - 10
def test_cross_day_pairing(self, db):
"""跨日配对:开仓和平仓在不同日期"""
open_rec = _make_record(db, direction="", offset="", price=4000, volume=1,
trade_date="2026-07-04", trade_time="09:30:00")
close_rec = _make_record(db, direction="", offset="", price=4100, volume=1,
close_pnl=1000, commission=20,
trade_date="2026-07-05", trade_time="14:00:00")
# 先配对 7-04 的数据(只有开仓,不会配对)
auto_pair_trades(db, "2026-07-04")
# 再配对 7-05 的数据
result = auto_pair_trades(db, "2026-07-05")
db.commit()
pairs = db.query(TradePair).all()
assert len(pairs) == 1
assert pairs[0].open_date == "2026-07-04"
assert pairs[0].close_date == "2026-07-05"
assert pairs[0].net_pnl == 970 # 1000 - 10 - 20
def test_multiple_opens_merged_position(self, db):
"""多次开仓合并为一个头寸FIFO 配对"""
open1 = _make_record(db, direction="", offset="", price=7800, volume=1, trade_time="09:30:00")
open2 = _make_record(db, direction="", offset="", price=7820, volume=1, trade_time="10:00:00")
close1 = _make_record(db, direction="", offset="", price=7900, volume=2,
close_pnl=1800, commission=20, trade_time="14:00:00")
result = auto_pair_trades(db, "2026-07-05")
db.commit()
assert result["created"] == 1
pairs = db.query(TradePair).all()
assert len(pairs) == 1
# 由于 close_volume(2) == open1.volume(1) + open2.volume(1),会一起配对
assert pairs[0].total_volume == 2
def test_manual_pair(self, db):
"""手动创建配对"""
open_rec = _make_record(db, direction="", offset="", price=7800, volume=2)
close_rec = _make_record(db, direction="", offset="", price=7900, volume=2,
close_pnl=2000, commission=20)
pair = create_manual_pair(db, [open_rec.id], [close_rec.id])
db.commit()
assert pair.direction == "long"
assert pair.open_price == 7800
assert pair.close_price == 7900
assert pair.net_pnl == 1970 # 2000 - 10 - 20
def test_delete_pair_keeps_records(self, db):
"""删除配对不删除原始交易记录"""
open_rec = _make_record(db, direction="", offset="", price=7800, volume=1)
close_rec = _make_record(db, direction="", offset="", price=7900, volume=1,
close_pnl=1000, commission=10)
pair = create_manual_pair(db, [open_rec.id], [close_rec.id])
db.commit()
delete_pair(db, pair.id)
db.commit()
assert db.query(TradePair).count() == 0
assert db.query(TradeRecord).count() == 2
class TestDailyTradesView:
"""按天交易视图测试"""
def test_get_daily_trades_with_pairs(self, db):
"""获取某日交易+配对+反思状态"""
open_rec = _make_record(db, direction="", offset="", price=7800, volume=1)
close_rec = _make_record(db, direction="", offset="", price=7900, volume=1,
close_pnl=1000, commission=10)
# 创建反思
pair = create_manual_pair(db, [open_rec.id], [close_rec.id])
reflection = TradeReflection(
trade_pair_id=pair.id,
trade_date="2026-07-05",
entry_reason="测试",
discipline_score=4,
)
db.add(reflection)
db.commit()
data = get_daily_trades_with_pairs(db, "2026-07-05")
assert data["stats"]["paired_count"] == 1
assert data["stats"]["unpaired_count"] == 0
assert len(data["pairs"]) == 1
assert data["pairs"][0]["reflection_status"] == "done"
class TestTagSystem:
"""标签系统测试"""
def test_tag_deduplication(self, db):
"""同一交易不能重复打相同标签"""
tag = TradeTag(name="追涨杀跌", category="operation", is_preset=True)
db.add(tag)
db.flush()
relation1 = TradeRecordTag(trade_pair_id=1, tag_id=tag.id)
db.add(relation1)
db.commit()
# 重复添加应该通过唯一约束失败
relation2 = TradeRecordTag(trade_pair_id=1, tag_id=tag.id)
db.add(relation2)
with pytest.raises(Exception):
db.commit()
db.rollback()
def test_preset_tags_initialization(self, db):
"""预设标签可以正常创建"""
tags = [
TradeTag(name="严格执行计划", category="discipline", is_preset=True),
TradeTag(name="突破交易", category="technical", is_preset=True),
]
db.add_all(tags)
db.commit()
assert db.query(TradeTag).filter_by(is_preset=True).count() == 2
class TestReflectionAIPrompt:
"""AI 反思分析提示词测试"""
def test_build_reflection_prompt_contains_key_info(self, db):
"""提示词包含交易关键信息和反思内容"""
open_rec = _make_record(db, direction="", offset="", price=7800, volume=1)
close_rec = _make_record(db, direction="", offset="", price=7900, volume=1,
close_pnl=1000, commission=10)
pair = create_manual_pair(db, [open_rec.id], [close_rec.id])
reflection = TradeReflection(
trade_pair_id=pair.id,
trade_date="2026-07-05",
entry_reason="突破买入",
entry_timing="good",
discipline_score=4,
free_reflection="执行较好",
)
db.add(reflection)
db.commit()
prompt = build_reflection_prompt(db, pair, reflection)
assert "7800" in prompt
assert "7900" in prompt
assert "突破买入" in prompt
assert "执行较好" in prompt
assert "JSON" in prompt
def test_parse_ai_response_with_json(self):
"""正常 JSON 响应解析"""
response = json.dumps({
"overall_evaluation": "综合评价",
"strengths": ["优点1"],
"weaknesses": ["不足1"],
"experience_suggestion": "建议",
"suggestion_type": "lesson",
})
parsed = _parse_ai_response(response)
assert parsed["overall_evaluation"] == "综合评价"
assert parsed["strengths"] == ["优点1"]
def test_parse_ai_response_with_markdown(self):
"""Markdown 代码块中的 JSON 解析"""
response = '```json\n{"overall_evaluation": "评价", "strengths": [], "weaknesses": [], "experience_suggestion": "", "suggestion_type": "tip"}\n```'
parsed = _parse_ai_response(response)
assert parsed["suggestion_type"] == "tip"
def test_parse_ai_response_fallback(self):
"""无法解析时返回原始内容"""
response = "这是 AI 的纯文本回复"
parsed = _parse_ai_response(response)
assert parsed["overall_evaluation"] == response
class TestExperienceSave:
"""经验保存测试"""
def test_save_suggestion_as_experience(self, db):
"""将 AI 分析建议保存为经验"""
open_rec = _make_record(db, direction="", offset="", price=7800, volume=1)
close_rec = _make_record(db, direction="", offset="", price=7900, volume=1,
close_pnl=1000, commission=10)
pair = create_manual_pair(db, [open_rec.id], [close_rec.id])
analysis = TradeAIAnalysis(
trade_pair_id=pair.id,
version=1,
overall_evaluation="评价",
experience_suggestion="重要经验",
suggestion_type="lesson",
)
db.add(analysis)
db.commit()
result = save_suggestion_as_experience(db, analysis.id, tags=["突破交易"])
assert result["success"] is True
exp = db.query(TradeExperience).first()
assert exp is not None
assert exp.exp_type == "lesson"
assert "重要经验" in exp.content
assert analysis.saved_to_experience is True
assert analysis.experience_id == exp.id
Loading…
Cancel
Save