""" 交易反思系统单元测试与集成测试 """ 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