alphax/tests/test_review_accuracy_pipeline.py
2026-05-26 21:10:22 +08:00

237 lines
9.1 KiB
Python

import json
import os
import sqlite3
import sys
from datetime import datetime, timedelta
import pytest
PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_DIR not in sys.path:
sys.path.insert(0, PROJECT_DIR)
from app.config import config_loader
from app.db import altcoin_db
from app.services import review_engine
@pytest.fixture
def temp_review_env(monkeypatch, tmp_path):
db_path = tmp_path / "review.db"
rules_path = tmp_path / "rules.yaml"
monkeypatch.setattr(altcoin_db, "DB_PATH", str(db_path))
monkeypatch.setattr(config_loader, "RULES_PATH", str(rules_path))
monkeypatch.setattr(review_engine, "get_conn", altcoin_db.get_conn)
config_loader._cache = None
config_loader._cache_mtime = None
rules_path.write_text(
"""
strategy:
mode: long_only
direction: 多头启动
confirm: {}
screener: {}
tracker: {}
signal_weights: {}
review:
hit_threshold_pct: 5.0
fail_threshold_pct: -3.0
missed_explosion_pct: 20.0
reverse_analysis: {}
learned_rules: []
meta:
strategy_version: v-test
""".strip(),
encoding="utf-8",
)
altcoin_db.init_db()
return db_path
def _insert_rec(db_path, **kwargs):
defaults = dict(
symbol="REV/USDT",
rec_time="2026-05-10T00:00:00",
rec_state="爆发",
rec_score=80,
entry_price=100.0,
stop_loss=95.0,
tp1=110.0,
tp2=120.0,
sector="",
signals=json.dumps(["1H 量价齐飞K(量3.7x)", "15min 回踩确认"], ensure_ascii=False),
signal_codes_json=json.dumps(["vp_fly_1h_current", "pullback_15m_confirm"], ensure_ascii=False),
signal_labels_json=json.dumps(["1H 量价齐飞K(量3.7x)", "15min 回踩确认"], ensure_ascii=False),
is_meme=0,
status="active",
current_price=80.0,
max_price=160.0,
min_price=70.0,
pnl_pct=-20.0,
max_pnl_pct=60.0,
max_drawdown_pct=-30.0,
hit_tp1_time="",
hit_tp2_time="",
stopped_out_time="",
expired_time="",
last_track_time="2026-05-13T00:00:00",
entry_plan_json=json.dumps({"entry_action": "可即刻买入", "entry_price": 100.0}, ensure_ascii=False),
action_status="可即刻买入",
execution_status="buy_now",
display_bucket="realtime",
lifecycle_state="buyable",
entry_triggered=1,
direction="多头启动",
)
defaults.update(kwargs)
conn = sqlite3.connect(db_path)
cols = ",".join(defaults.keys())
placeholders = ",".join(["?"] * len(defaults))
cur = conn.execute(f"INSERT INTO recommendation ({cols}) VALUES ({placeholders})", tuple(defaults.values()))
conn.commit()
conn.close()
return cur.lastrowid
def test_reviewable_recommendations_exclude_untriggered_watch_pool(temp_review_env):
db_path = str(temp_review_env)
_insert_rec(
db_path,
symbol="WAIT/USDT",
action_status="等回踩",
execution_status="wait_pullback",
display_bucket="watch_pool",
entry_triggered=0,
)
_insert_rec(db_path, symbol="BUY/USDT")
rows = review_engine._get_reviewable_recommendations(datetime.fromisoformat("2026-05-12T01:00:00"))
assert [row["symbol"] for row in rows] == ["BUY/USDT"]
def test_review_uses_48h_price_tracking_window_not_current_price(temp_review_env):
db_path = str(temp_review_env)
rec_id = _insert_rec(db_path)
conn = sqlite3.connect(db_path)
for hours, price in [(1, 101.0), (5, 106.0), (47, 103.0), (60, 150.0)]:
ts = datetime.fromisoformat("2026-05-10T00:00:00") + timedelta(hours=hours)
conn.execute(
"INSERT INTO price_tracking (rec_id, symbol, track_time, price, pnl_pct) VALUES (?, 'REV/USDT', ?, ?, 0)",
(rec_id, ts.isoformat(), price),
)
conn.commit()
conn.row_factory = sqlite3.Row
rec = dict(conn.execute("SELECT * FROM recommendation WHERE id=?", (rec_id,)).fetchone())
conn.close()
review = review_engine.review_recommendation(rec)
assert review["outcome"] == "爆发"
assert review["pnl_48h"] == 3.0
assert review["max_pnl_48h"] == 6.0
assert review["window_metrics"]["source"] == "price_tracking"
def test_review_updates_signal_performance_by_code_not_label(temp_review_env):
db_path = str(temp_review_env)
rec_id = _insert_rec(db_path)
conn = sqlite3.connect(db_path)
conn.execute(
"INSERT INTO price_tracking (rec_id, symbol, track_time, price, pnl_pct) VALUES (?, 'REV/USDT', '2026-05-10T01:00:00', 106.0, 6.0)",
(rec_id,),
)
conn.commit()
conn.row_factory = sqlite3.Row
rec = dict(conn.execute("SELECT * FROM recommendation WHERE id=?", (rec_id,)).fetchone())
conn.close()
review_engine.review_recommendation(rec)
conn = sqlite3.connect(db_path)
rows = conn.execute("SELECT signal_type,total_count,hit_count FROM signal_performance ORDER BY signal_type").fetchall()
conn.close()
stats = {row[0]: (row[1], row[2]) for row in rows}
assert stats["vp_fly_1h_current"] == (1, 1)
assert "1H 量价齐飞K(量3.7x)" not in stats
def test_daily_factor_weight_governance_promotes_and_eliminates(monkeypatch, temp_review_env):
changes = []
monkeypatch.setattr(review_engine, "update_signal_weight", lambda signal, weight: changes.append((signal, weight)))
monkeypatch.setattr(review_engine, "get_signal_weights", lambda: {
"good_factor": {"category": "前瞻", "total_count": 12, "hit_rate": 70, "avg_pnl": 4.2, "weight": 1.0},
"bad_factor": {"category": "前瞻", "total_count": 20, "hit_rate": 5, "avg_pnl": -2.0, "weight": 1.2},
"thin_factor": {"category": "PA", "total_count": 2, "hit_rate": 100, "avg_pnl": 6.0, "weight": 1.0},
})
result = review_engine._apply_daily_factor_weight_governance()
assert ("good_factor", 2.8) in changes
assert ("bad_factor", 0.0) in changes
assert all(signal != "thin_factor" for signal, _ in changes)
actions = {item["signal"]: item["action"] for item in result}
assert actions["good_factor"] == "升权"
assert actions["bad_factor"] == "淘汰"
def test_review_without_tracking_is_not_counted_as_signal_performance(temp_review_env):
db_path = str(temp_review_env)
rec_id = _insert_rec(db_path)
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
rec = dict(conn.execute("SELECT * FROM recommendation WHERE id=?", (rec_id,)).fetchone())
conn.close()
review = review_engine.review_recommendation(rec)
assert review["outcome"] == "样本不足"
assert review["skipped"] is True
assert review["window_metrics"]["source"] == "insufficient_tracking"
conn = sqlite3.connect(db_path)
rows = conn.execute("SELECT signal_type,total_count FROM signal_performance").fetchall()
review_rows = conn.execute("SELECT outcome, lesson FROM review_log WHERE rec_id=?", (rec_id,)).fetchall()
conn.close()
assert rows == []
assert review_rows[0][0] == "样本不足"
assert "不进入推荐绩效/信号权重" in review_rows[0][1]
def test_run_review_does_not_double_upsert_reverse_rules(monkeypatch, temp_review_env):
calls = []
monkeypatch.setattr(review_engine, "_get_reviewable_recommendations", lambda now=None: [])
monkeypatch.setattr(review_engine, "adjust_signal_weights", lambda: [])
monkeypatch.setattr(review_engine, "_deprecate_low_performance_signals", lambda: [])
monkeypatch.setattr(review_engine, "scan_missed_explosions", lambda: [])
monkeypatch.setattr(review_engine, "_extract_rules_from_review", lambda: [])
monkeypatch.setattr(review_engine, "get_signal_weights", lambda: {})
monkeypatch.setattr(review_engine, "_compute_effect_summary", lambda now, lookback_days=7: {"hit_rate_pct": 0, "avg_pnl": 0})
monkeypatch.setattr(review_engine, "_scan_stable_fiat_pollution", lambda now, lookback_days=7: {})
monkeypatch.setattr(review_engine, "_build_dual_attribution", lambda results, effect_summary: {
"success_analysis": {"sample_count": 0, "top_success_factors": []},
"failure_analysis": {"sample_count": 0, "failure_types": []},
"candidate_rules": [],
"release_decision": "hold",
"release_reason": "样本不足",
"confidence_level": "low",
"promotion_state": "research_only",
})
monkeypatch.setattr(review_engine, "refresh_strategy_candidate_performance", lambda: [])
monkeypatch.setattr(review_engine, "_release_candidate_rules_if_ready", lambda dual, effect: {
"released": False,
"release_decision": "hold",
"release_reason": "样本不足",
"released_rules": [],
"new_version": "",
})
monkeypatch.setattr(review_engine.reverse_analysis, "run_reverse_analysis", lambda: {
"new_rules": [{"candidate_id": 77, "description": "逆向候选", "type": "bonus", "conditions": {"has_ignition_point": True}}],
})
monkeypatch.setattr(review_engine, "upsert_strategy_rule_candidate", lambda **kwargs: calls.append(kwargs) or 88)
result = review_engine.run_review(push_enabled=False)
assert result["reverse_analysis"]["new_rules"][0]["candidate_id"] == 77
assert calls == []