import os 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.db import altcoin_db from app.services import review_engine from app.config import config_loader @pytest.fixture def temp_db_and_rules(monkeypatch, tmp_path): db_path = tmp_path / "altcoin_monitor.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: 多头启动 allow_short: false screener: {} confirm: {} tracker: {} signal_weights: {} review: hit_threshold_pct: 5.0 fail_threshold_pct: -3.0 missed_explosion_pct: 20.0 reverse_analysis: {} learned_rules: [] meta: version: 1 strategy_version: v_next strategy_revision_started_at: '2026-04-30T10:00:00' strategy_revision_note: '静K蓄力改版开始' """.strip(), encoding="utf-8", ) altcoin_db.init_db() return db_path, rules_path def _insert_recommendation(conn, rec_id, symbol, rec_time): conn.execute( """ INSERT INTO recommendation ( id, symbol, rec_time, rec_state, rec_score, entry_price, stop_loss, tp1, tp2, sector, signals, is_meme, status, current_price, max_price, min_price, pnl_pct, max_pnl_pct, max_drawdown_pct, hit_tp1_time, hit_tp2_time, stopped_out_time, expired_time, last_track_time, entry_plan_json, action_status, execution_status, display_bucket, lifecycle_state, entry_triggered, direction ) VALUES (?, ?, ?, '加速', 8, 1.0, 0, 0, 0, '', '[]', 0, 'active', 1.0, 1.0, 1.0, 0, 0, 0, '', '', '', '', ?, '{"entry_action":"可即刻买入","entry_price":1.0}', '可即刻买入', 'buy_now', 'realtime', 'buyable', 1, '多头启动') """, (rec_id, symbol, rec_time, rec_time), ) def _insert_review(conn, rec_id, symbol, review_time, outcome, pnl_48h): conn.execute( """ INSERT INTO review_log ( rec_id, symbol, review_time, outcome, pnl_48h, max_pnl_48h, triggered_signals, hit_signals, miss_signals, lesson ) VALUES (?, ?, ?, ?, ?, ?, '[]', '[]', '[]', '') """, (rec_id, symbol, review_time, outcome, pnl_48h, pnl_48h), ) def _insert_missed(conn, symbol, detect_time, gain_pct): conn.execute( """ INSERT INTO missed_explosions ( symbol, detect_time, price_at_detect, price_before, gain_pct, reason_missed, features_detected, lesson ) VALUES (?, ?, 1.0, 0.8, ?, '粗筛未过', '[]', '') """, (symbol, detect_time, gain_pct), ) def test_revision_marker_filters_effect_summary(temp_db_and_rules): conn = altcoin_db.get_conn() _insert_recommendation(conn, 1, 'OLD/USDT', '2026-04-29T09:00:00') _insert_recommendation(conn, 2, 'NEW/USDT', '2026-04-30T11:00:00') _insert_review(conn, 1, 'OLD/USDT', '2026-04-29T12:00:00', '失败', -6.0) _insert_review(conn, 2, 'NEW/USDT', '2026-04-30T12:00:00', '爆发', 12.0) conn.commit() conn.close() now = datetime.fromisoformat('2026-04-30T13:00:00') summary = review_engine._compute_effect_summary(now, lookback_days=7) assert summary['review_count_window'] == 1 assert summary['hit_rate_pct'] == 100.0 assert summary['fail_rate_pct'] == 0.0 assert summary['avg_pnl'] == 12.0 def test_revision_marker_filters_reviewable_recommendations(temp_db_and_rules): conn = altcoin_db.get_conn() _insert_recommendation(conn, 1, 'OLD/USDT', '2026-04-29T09:00:00') _insert_recommendation(conn, 2, 'NEW/USDT', '2026-04-30T11:00:00') conn.commit() conn.close() reviewable = review_engine._get_reviewable_recommendations(datetime.fromisoformat('2026-05-01T13:00:00')) symbols = [row['symbol'] for row in reviewable] assert symbols == ['NEW/USDT'] def test_revision_marker_filters_review_stats_and_missed_explosions(temp_db_and_rules): conn = altcoin_db.get_conn() _insert_review(conn, 1, 'OLD/USDT', '2026-04-29T12:00:00', '失败', -6.0) _insert_review(conn, 2, 'NEW/USDT', '2026-04-30T12:00:00', '爆发', 12.0) _insert_missed(conn, 'OLDMISS/USDT', '2026-04-29T15:00:00', 35.0) _insert_missed(conn, 'NEWMISS/USDT', '2026-04-30T15:00:00', 28.0) conn.commit() conn.close() stats = altcoin_db.get_review_stats() review_symbols = [item['symbol'] for item in stats['reviews']] missed_symbols = [item['symbol'] for item in stats['missed_explosions']] # get_review_stats() 不再按 revision_started_at 过滤 review_log, # 页面展示需要累积全部复盘数据。revision marker 的过滤只在 # review_engine._get_reviewable_recommendations() 中生效。 assert set(review_symbols) == {'NEW/USDT', 'OLD/USDT'} assert set(missed_symbols) == {'NEWMISS/USDT', 'OLDMISS/USDT'}