209 lines
8.4 KiB
Python
209 lines
8.4 KiB
Python
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"
|