146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
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'}
|