alphax/tests/test_actionable_active_recommendations.py
2026-05-20 00:57:46 +08:00

417 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
from datetime import datetime
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=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
rec_state='加速',
rec_score=40,
entry_price=100.0,
stop_loss=95.0,
tp1=110.0,
tp2=118.0,
sector='AI',
signals='["🟢 15min即刻入场信号"]',
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=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
entry_plan_json='{"entry_price":100,"entry_action":"可即刻买入","entry_trigger_confirmed":true,"risk_reward_ok":true,"stop_loss":95,"tp1":110,"rr1":2}',
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'}
buy = next(row for row in rows if row['symbol'] == 'BUY/USDT')
wait = next(row for row in rows if row['symbol'] == 'WAIT/USDT')
assert buy['discovery_state'] == '加速'
assert buy['trade_stage'] == 'buy_now'
assert buy['is_executable_now'] is True
assert wait['trade_stage'] == 'wait_pullback'
assert wait['is_trade_candidate'] is True
meta = altcoin_db.get_active_recommendations_deduped(actionable_only=False, with_meta=True)
assert meta['summary']['executable_now'] == 1
assert meta['summary']['planned_entry'] == 1
assert meta['summary']['watch_pool'] == 2
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'] == 0
assert stats['active_avg_pnl'] == 0
assert stats['active_success_count'] == 0
assert stats['active_failed_count'] == 0
assert stats['active_pending_count'] == 0
def test_link_can_be_able_to_buy_without_signal_degradation_when_context_is_consistent(temp_db):
_insert_recommendation(
temp_db,
symbol='LINK/USDT',
action_status='可即刻买入',
entry_plan_json='{"entry_price": 9.74, "entry_action": "可即刻买入", "entry_trigger_confirmed": true, "risk_reward_ok": true, "rr1": 1.6, "stop_loss": 9.253, "tp1": 10.5192, "opportunity_level": "short_swing", "opportunity_level_label": "短波段", "max_action": "buy_now"}',
signals='["🟢 15min即刻入场信号", "1H 量价齐飞K(量3.7x)"]',
current_price=9.74,
entry_price=9.74,
max_price=9.74,
min_price=9.74,
rec_time=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'LINK/USDT')
assert target['execution_status'] == 'buy_now'
assert target['action_status'] == '可即刻买入'
assert target['opportunity_level'] == 'short_swing'
assert target['observe_reason'] != '信号偏弱'
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'] == 0
assert live['executed_pnl_sum'] == 0
assert live['executed_avg_pnl'] == 0
assert live['actionable_pnl_sum'] == 0
assert live['actionable_avg_pnl'] == 0
assert live['actionable_success_count'] == 0
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_history_api_excludes_untriggered_observe_wait_and_invalid_losses(temp_db):
_insert_recommendation(
temp_db,
symbol='OBSLOSS/USDT',
action_status='持有',
status='active',
entry_plan_json='{"entry_action": "继续观察"}',
pnl_pct=-4.0,
max_drawdown_pct=-6.0,
)
_insert_recommendation(
temp_db,
symbol='WAITLOSS/USDT',
action_status='等回踩',
status='active',
entry_plan_json='{"entry_action": "等回踩", "entry_price": 95}',
pnl_pct=-5.0,
max_drawdown_pct=-7.0,
)
_insert_recommendation(
temp_db,
symbol='INVLOSS/USDT',
action_status='衰减',
status='active',
entry_plan_json='{"entry_action": "继续观察"}',
pnl_pct=-6.0,
max_drawdown_pct=-8.0,
)
page = altcoin_db.get_all_recommendations(limit=20, decision_only=True, with_meta=True)
assert page['items'] == []
assert page['total'] == 0
assert page['summary']['total'] == 0
assert page['summary']['failure_count'] == 0
assert page['summary']['total_pnl'] == 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