import os import sys import pytest from fastapi.testclient import TestClient 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.web import web_server @pytest.fixture def temp_db(monkeypatch, tmp_path): db_path = tmp_path / "altcoin_monitor.db" monkeypatch.setattr(altcoin_db, "DB_PATH", str(db_path)) monkeypatch.setattr(web_server, "init_db", altcoin_db.init_db) monkeypatch.setattr(web_server, "get_stats", altcoin_db.get_stats) monkeypatch.setattr(web_server, "get_active_recommendations", altcoin_db.get_active_recommendations) monkeypatch.setattr(web_server, "get_active_recommendations_deduped", altcoin_db.get_active_recommendations_deduped) altcoin_db.init_db() return db_path def _insert_recommendation(db_path, **kwargs): defaults = dict( symbol='AAA/USDT', rec_time='2026-04-30T10:00:00', rec_state='加速', rec_score=8, entry_price=100.0, stop_loss=95.0, tp1=110.0, tp2=118.0, sector='AI', signals='[]', is_meme=0, status='active', current_price=100.0, max_price=104.0, min_price=98.0, pnl_pct=0.0, max_pnl_pct=4.0, max_drawdown_pct=-1.0, hit_tp1_time='', hit_tp2_time='', stopped_out_time='', expired_time='', last_track_time='2026-04-30T10:05:00', entry_plan_json='{}', action_status='持有', direction='多头启动', strategy_version='v1.2', ) defaults.update(kwargs) conn = altcoin_db.sqlite3.connect(str(db_path)) conn.execute( ''' INSERT INTO recommendation ( 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, direction, strategy_version ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( defaults['symbol'], defaults['rec_time'], defaults['rec_state'], defaults['rec_score'], defaults['entry_price'], defaults['stop_loss'], defaults['tp1'], defaults['tp2'], defaults['sector'], defaults['signals'], defaults['is_meme'], defaults['status'], defaults['current_price'], defaults['max_price'], defaults['min_price'], defaults['pnl_pct'], defaults['max_pnl_pct'], defaults['max_drawdown_pct'], defaults['hit_tp1_time'], defaults['hit_tp2_time'], defaults['stopped_out_time'], defaults['expired_time'], defaults['last_track_time'], defaults['entry_plan_json'], defaults['action_status'], defaults['direction'], defaults['strategy_version'], ), ) conn.commit() conn.close() def test_active_api_only_returns_actionable_recommendations(temp_db): _insert_recommendation( temp_db, symbol='BUY/USDT', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}' ) _insert_recommendation( temp_db, symbol='WAIT/USDT', action_status='等回踩', entry_plan_json='{"entry_action": "等回踩"}' ) _insert_recommendation( temp_db, symbol='OBS/USDT', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}' ) _insert_recommendation( temp_db, symbol='INV/USDT', action_status='衰减', entry_plan_json='{"entry_action": "可即刻买入"}' ) client = TestClient(web_server.app) resp = client.get('/api/recommendations/active') assert resp.status_code == 200 rows = resp.json() assert {row['symbol'] for row in rows} == {'WAIT/USDT', 'BUY/USDT'} assert {row['execution_status'] for row in rows} == {'buy_now', 'wait_pullback'} def test_stats_only_count_actionable_active_recommendations(temp_db): _insert_recommendation( temp_db, symbol='BUY/USDT', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=5.0, max_pnl_pct=5.0, ) _insert_recommendation( temp_db, symbol='WAIT/USDT', action_status='等回踩', entry_plan_json='{"entry_action": "等回踩"}', pnl_pct=1.0, ) _insert_recommendation( temp_db, symbol='OBS/USDT', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}', pnl_pct=9.0, max_pnl_pct=9.0, ) _insert_recommendation( temp_db, symbol='INV/USDT', action_status='衰减', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=-6.0, max_drawdown_pct=-6.0, ) stats = altcoin_db.get_stats() assert stats['active_count'] == 2 assert stats['raw_active_count'] == 4 assert stats['active_pnl_sum'] == 5.0 assert stats['active_avg_pnl'] == 5.0 assert stats['active_success_count'] == 1 assert stats['active_failed_count'] == 0 assert stats['active_pending_count'] == 0 def test_stats_api_exposes_separate_live_and_history_sections(temp_db): _insert_recommendation( temp_db, symbol='BUY/USDT', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=5.0, max_pnl_pct=5.0, ) _insert_recommendation( temp_db, symbol='WAIT/USDT', action_status='等回踩', entry_plan_json='{"entry_action": "等回踩"}', pnl_pct=1.0, ) _insert_recommendation( temp_db, symbol='OBS/USDT', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}', pnl_pct=9.0, max_pnl_pct=9.0, ) _insert_recommendation( temp_db, symbol='INV/USDT', action_status='衰减', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=-6.0, max_drawdown_pct=-6.0, ) client = TestClient(web_server.app) resp = client.get('/api/stats') assert resp.status_code == 200 stats = resp.json() live = stats['live_overview'] assert live['actionable_count'] == 2 assert live['executed_trade_count'] == 1 assert live['executed_pnl_sum'] == 5.0 assert live['executed_avg_pnl'] == 5.0 assert live['actionable_pnl_sum'] == 5.0 assert live['actionable_avg_pnl'] == 5.0 assert live['actionable_success_count'] == 1 assert live['actionable_failed_count'] == 0 assert live['actionable_pending_count'] == 0 assert live['raw_active_count'] == 4 assert stats['history_overview']['success_count'] == 0 assert stats['history_overview']['failed_count'] == 0 assert 'pending_count' not in stats['history_overview'] assert stats['history_overview']['recommendation_success_rate'] == pytest.approx(0.0) def test_history_overview_only_counts_real_tp_and_stopout_results(temp_db): _insert_recommendation( temp_db, symbol='TP1/USDT', status='hit_tp1', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=7.0, max_pnl_pct=7.0, ) _insert_recommendation( temp_db, symbol='TP2/USDT', status='hit_tp2', action_status='等回踩', entry_plan_json='{"entry_action": "等回踩"}', pnl_pct=12.0, max_pnl_pct=12.0, ) _insert_recommendation( temp_db, symbol='STOP/USDT', status='stopped_out', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=-4.0, max_drawdown_pct=-6.0, ) _insert_recommendation( temp_db, symbol='MAXONLY/USDT', status='active', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}', pnl_pct=1.5, max_pnl_pct=9.0, ) _insert_recommendation( temp_db, symbol='DDONLY/USDT', status='active', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}', pnl_pct=-1.0, max_drawdown_pct=-7.0, ) client = TestClient(web_server.app) resp = client.get('/api/stats') assert resp.status_code == 200 stats = resp.json() assert stats['history_overview']['success_count'] == 2 assert stats['history_overview']['failed_count'] == 1 assert stats['history_overview']['recommendation_success_rate'] == pytest.approx(66.7) assert 'pending_count' not in stats['history_overview'] def test_history_avg_pnl_only_uses_real_tp_and_stopout_samples(temp_db): _insert_recommendation( temp_db, symbol='TP1AVG/USDT', status='hit_tp1', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=6.0, max_pnl_pct=6.0, ) _insert_recommendation( temp_db, symbol='STOPAVG/USDT', status='stopped_out', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', pnl_pct=-4.0, max_drawdown_pct=-6.0, ) _insert_recommendation( temp_db, symbol='EXPIREDAVG/USDT', status='expired', action_status='持有', entry_plan_json='{"entry_action": "继续观察"}', pnl_pct=30.0, max_pnl_pct=30.0, ) client = TestClient(web_server.app) resp = client.get('/api/stats') assert resp.status_code == 200 stats = resp.json() assert stats['history_overview']['avg_pnl_pct'] == pytest.approx(1.0) def test_version_filter_labels_use_plan_not_executable_to_avoid_wait_pullback_confusion(temp_db): _insert_recommendation( temp_db, symbol='BUY/USDT', action_status='可即刻买入', entry_plan_json='{"entry_action": "可即刻买入"}', strategy_version='v1.7.3', ) _insert_recommendation( temp_db, symbol='WAIT/USDT', action_status='等回踩', entry_plan_json='{"entry_action": "等回踩"}', strategy_version='v1.7.3', ) client = TestClient(web_server.app) versions = client.get('/api/versions?view=active').json() v173 = next(v for v in versions if v['version'] == 'v1.7.3') assert v173['count'] == 2 monkeypatch = pytest.MonkeyPatch() monkeypatch.setattr(web_server, "_require_active_subscription", lambda altcoin_session='': ({"id": 1}, {"plan_code": "test"})) try: html = client.get('/app').text finally: monkeypatch.undo() # v1.7.7: 新看板没有 watch tab,使用 version dropdown assert 'version-select' in html or '全部版本' in html assert '实时推荐' in html assert '历史推荐' in html # v1.7.7: 新看板版本下拉用 (${v.count}) 格式 + 默认选中最新版本 assert 'v.count' in html