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

288 lines
10 KiB

This file contains ambiguous Unicode characters!

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

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