import os import sqlite3 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(tmp_path, monkeypatch): db_path = tmp_path / "altcoin_monitor.db" monkeypatch.setattr(altcoin_db, "DB_PATH", str(db_path)) altcoin_db.init_db() yield db_path def _require_columns(conn, columns): rows = conn.execute("PRAGMA table_info(recommendation)").fetchall() existing = {row[1] for row in rows} for column in columns: assert column in existing, f"missing column: {column}" def _insert_recommendation(conn, **overrides): row = { "symbol": "TEST/USDT", "rec_time": "2026-04-30T10:00:00", "rec_state": "加速", "rec_score": 15.5, "entry_price": 1.25, "stop_loss": 1.1, "tp1": 1.4, "tp2": 1.55, "sector": "AI", "signals": "[]", "is_meme": 0, "status": "active", "current_price": 1.32, "max_price": 1.36, "max_drawdown_pct": -2.5, "max_pnl_pct": 5.2, "pnl_pct": 3.1, "last_track_time": "2026-04-30T11:00:00", "action_status": "可即刻买入", "entry_plan_json": '{"entry_action": "可即刻买入", "entry_price": 1.25}', "direction": "多头启动", "force_reason": "", "base_state": "加速", "sector_signal_count": 0, "strategy_version": "v11.1", "market_context_json": "{}", "derivatives_context_json": "{}", "sector_context_json": "{}", } row.update(overrides) 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, max_drawdown_pct, max_pnl_pct, pnl_pct, last_track_time, action_status, entry_plan_json, direction, force_reason, base_state, sector_signal_count, strategy_version, market_context_json, derivatives_context_json, sector_context_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( row["symbol"], row["rec_time"], row["rec_state"], row["rec_score"], row["entry_price"], row["stop_loss"], row["tp1"], row["tp2"], row["sector"], row["signals"], row["is_meme"], row["status"], row["current_price"], row["max_price"], row["max_drawdown_pct"], row["max_pnl_pct"], row["pnl_pct"], row["last_track_time"], row["action_status"], row["entry_plan_json"], row["direction"], row["force_reason"], row["base_state"], row["sector_signal_count"], row["strategy_version"], row["market_context_json"], row["derivatives_context_json"], row["sector_context_json"], ), ) def test_active_recommendations_expose_enriched_context(temp_db): conn = altcoin_db.get_conn() _require_columns(conn, [ "market_context_json", "derivatives_context_json", "sector_context_json", ]) _insert_recommendation( conn, symbol="PNT/USDT", force_reason="静K蓄力旁路", base_state="蓄力", sector_signal_count=2, market_context_json='{"volume_24h": 12000000, "turnover_acceleration_1h": 2.8, "turnover_acceleration_4h": 1.6, "change_24h": 8.2}', derivatives_context_json='{"funding_rate": 0.0008, "top_trader_long_pct": 62.5, "top_trader_long_short_ratio": 1.7}', sector_context_json='{"sectors": ["AI", "Infra"], "hot_sectors": ["AI"], "leader_symbol": "WLD/USDT", "leader_move_pct": 9.6}', ) conn.commit() conn.close() rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False) assert len(rows) == 1 item = rows[0] assert item["force_reason"] == "静K蓄力旁路" assert item["base_state"] == "蓄力" assert item["sector_signal_count"] == 2 assert item["market_context"]["turnover_acceleration_1h"] == 2.8 assert item["market_context"]["change_24h"] == 8.2 assert item["derivatives_context"]["top_trader_long_pct"] == 62.5 assert item["sector_context"]["leader_symbol"] == "WLD/USDT" assert item["sector_context"]["hot_sectors"] == ["AI"] def test_stats_exposes_market_context_summary(temp_db): conn = altcoin_db.get_conn() _require_columns(conn, [ "market_context_json", "derivatives_context_json", "sector_context_json", ]) _insert_recommendation( conn, symbol="PNT/USDT", force_reason="静K蓄力旁路", base_state="蓄力", sector_signal_count=1, market_context_json='{"volume_24h": 12000000, "turnover_acceleration_1h": 2.8, "turnover_acceleration_4h": 1.6}', derivatives_context_json='{"funding_rate": 0.0008, "top_trader_long_pct": 62.5, "top_trader_long_short_ratio": 1.7}', sector_context_json='{"sectors": ["AI"], "hot_sectors": ["AI"], "leader_symbol": "WLD/USDT", "leader_move_pct": 9.6}', ) _insert_recommendation( conn, symbol="AI/USDT", sector="AI,Gaming", force_reason="纯板块联动降级", base_state="加速", sector_signal_count=2, market_context_json='{"volume_24h": 20000000, "turnover_acceleration_1h": 3.2, "turnover_acceleration_4h": 2.1}', derivatives_context_json='{"funding_rate": 0.0012, "top_trader_long_pct": 66.0, "top_trader_long_short_ratio": 1.9}', sector_context_json='{"sectors": ["AI", "Gaming"], "hot_sectors": ["AI", "Gaming"], "leader_symbol": "TAO/USDT", "leader_move_pct": 12.4}', ) conn.commit() conn.close() stats = altcoin_db.get_stats() market = stats["market_context_overview"] assert market["actionable_sample_count"] == 2 assert market["avg_turnover_acceleration_1h"] == 3.0 assert market["avg_funding_rate"] == 0.001 assert market["avg_top_trader_long_pct"] == 64.2 assert market["top_hot_sectors"][0] == {"sector": "AI", "count": 2} assert market["top_hot_sectors"][1] == {"sector": "Gaming", "count": 1} def test_stats_api_returns_market_context_overview(temp_db): conn = altcoin_db.get_conn() _require_columns(conn, ["market_context_json", "derivatives_context_json", "sector_context_json"]) _insert_recommendation( conn, symbol="PNT/USDT", force_reason="静K蓄力旁路", base_state="蓄力", sector_signal_count=2, market_context_json='{"volume_24h": 12000000, "turnover_acceleration_1h": 2.8, "turnover_acceleration_4h": 1.6}', derivatives_context_json='{"funding_rate": 0.0008, "top_trader_long_pct": 62.5, "top_trader_long_short_ratio": 1.7}', sector_context_json='{"sectors": ["AI"], "hot_sectors": ["AI"], "leader_symbol": "WLD/USDT", "leader_move_pct": 9.6}', ) conn.commit() conn.close() client = TestClient(web_server.app) resp = client.get("/api/stats") assert resp.status_code == 200 data = resp.json() assert "market_context_overview" in data overview = data["market_context_overview"] assert overview["actionable_sample_count"] == 1 assert overview["top_hot_sectors"][0]["sector"] == "AI" def test_recommendations_api_and_page_expose_context_fields(temp_db): conn = altcoin_db.get_conn() _insert_recommendation( conn, symbol="PNT/USDT", force_reason="静K蓄力旁路", base_state="蓄力", sector_signal_count=2, market_context_json='{"volume_24h": 12000000, "turnover_acceleration_1h": 2.8, "turnover_acceleration_4h": 1.6, "change_24h": 8.2}', derivatives_context_json='{"funding_rate": 0.0008, "open_interest_change_24h": 14.5, "top_trader_long_pct": 62.5, "top_trader_long_short_ratio": 1.7}', sector_context_json='{"sectors": ["AI", "Infra"], "hot_sectors": ["AI"], "leader_symbol": "WLD/USDT", "leader_move_pct": 9.6}', ) conn.commit() conn.close() client = TestClient(web_server.app) api_resp = client.get("/api/recommendations/active?actionable_only=false") assert api_resp.status_code == 200 rows = api_resp.json() assert rows[0]["market_context"]["turnover_acceleration_1h"] == 2.8 assert rows[0]["derivatives_context"]["open_interest_change_24h"] == 14.5 assert rows[0]["sector_context"]["leader_symbol"] == "WLD/USDT"