import json
import os
import sys
from datetime import datetime, timedelta
from unittest.mock import patch
import pandas as pd
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.services import event_driven_screener as ed
def test_symbol_extraction_filters_usdt_suffix_and_pollution():
title = "Binance Futures Will Launch ABCUSDT, XYZUSDT and USD1USDT USDⓈ-Margined Perpetual Contracts"
symbols = ed._symbol_from_title(title)
assert "ABC/USDT" in symbols
assert "XYZ/USDT" in symbols
assert "ABCUSDT/USDT" not in symbols
assert "USD1/USDT" not in symbols
def test_recent_time_window_rejects_old_news():
assert ed._is_recent(datetime.now() - timedelta(hours=2), 3) is True
assert ed._is_recent(datetime.now() - timedelta(hours=8), 3) is False
assert ed._is_recent(None, 3) is False
def test_classify_major_listing_as_s_level_and_negative_as_risk():
level, event_type = ed.classify_event("Binance Futures Will Launch ABCUSDT USDⓈ-Margined Perpetual Contracts")
assert level == "S"
assert event_type == "major_listing_or_contract"
level, event_type = ed.classify_event("Binance Will Delist ABC on 2026-05-07")
assert level == "RISK"
assert event_type == "risk_negative"
def test_store_events_deduplicates_by_hash(tmp_path):
# 使用真实DB表,但同一事件重复插入只保留一次
event = {
"source": "binance_listing",
"symbol": "ABC/USDT",
"title": "Binance Futures Will Launch ABCUSDT USDⓈ-Margined Perpetual Contracts",
"url": "https://example.com",
"published_at": datetime.now(),
"importance": "S",
"event_type": "major_listing_or_contract",
"raw": {"id": 1},
}
first = ed.store_events([event])
second = ed.store_events([event])
assert len(first) in (0, 1) # 若本地DB已有同事件,允许0
assert second == []
def test_quick_technical_check_rejects_old_overheated_gain():
event = {
"source": "binance_listing",
"symbol": "ABC/USDT",
"title": "Binance Futures Will Launch ABCUSDT USDⓈ-Margined Perpetual Contracts",
"importance": "S",
}
with patch.object(ed, "_ticker_info", return_value={"price": 1.0, "change_24h": 35.0, "volume_24h": 10000000}):
result = ed.quick_technical_check(event)
assert result["decision"] == "risk"
assert "不追高" in result["reason"]
def test_theme_expansion_spreads_ton_news_to_ecosystem_symbols():
event = {
"source": "coingecko_trending",
"symbol": "TON/USDT",
"title": "Telegram becomes the main driver of the TON ecosystem and cuts TON fees",
"url": "https://example.com/ton",
"published_at": datetime.now(),
"importance": "B",
"event_type": "market_heat",
"raw": {},
}
expanded = ed.expand_theme_events([event])
by_symbol = {e["symbol"]: e for e in expanded}
assert "TON/USDT" in by_symbol
assert "NOT/USDT" in by_symbol
assert "DOGS/USDT" in by_symbol
assert by_symbol["DOGS/USDT"]["importance"] == "A"
assert by_symbol["DOGS/USDT"]["event_type"] == "theme_expansion"
assert "主题扩散:ton_ecosystem" in by_symbol["DOGS/USDT"]["title"]
def test_wublock_atom_feed_events_are_parsed_and_symbolized(monkeypatch):
xml = """
SUI ecosystem project announces major upgrade and airdrop
2026-05-16T10:00:00Z
"""
class Resp:
status_code = 200
content = xml.encode("utf-8")
monkeypatch.setattr(ed, "_now", lambda: datetime(2026, 5, 16, 18, 0, 0))
with patch.object(ed.requests, "get", return_value=Resp()):
events = ed.fetch_rss_events("wublock123", {
"enabled": True,
"type": "rss",
"url": "https://www.wublock123.com/feed",
"weight": "A",
"symbol_aliases": {"sui": "SUI"},
})
assert len(events) == 1
assert events[0]["source"] == "wublock123"
assert events[0]["symbol"] == "SUI/USDT"
assert events[0]["importance"] == "A"
assert events[0]["url"] == "https://www.wublock123.com/example"
def test_panews_rss_feed_events_are_parsed_and_symbolized(monkeypatch):
xml = """
-
Solana 生态 Meme 项目成交量大幅上升,SUI 同步活跃
https://www.panewslab.com/example
Sat, 16 May 2026 10:00:00 GMT
"""
class Resp:
status_code = 200
content = xml.encode("utf-8")
monkeypatch.setattr(ed, "_now", lambda: datetime(2026, 5, 16, 18, 0, 0))
with patch.object(ed.requests, "get", return_value=Resp()):
events = ed.fetch_rss_events("panewslab", {
"enabled": True,
"type": "rss",
"url": "https://www.panewslab.com/rss.xml?lang=zh&type=NORMAL%2CNEWS",
"weight": "A",
"symbol_aliases": {"solana": "SOL", "sui": "SUI"},
})
assert {e["symbol"] for e in events} == {"SOL/USDT", "SUI/USDT"}
assert {e["source"] for e in events} == {"panewslab"}
assert all(e["importance"] == "A" for e in events)
def _fake_ohlcv(rows=60):
return pd.DataFrame({
"timestamp": pd.date_range("2026-05-01", periods=rows, freq="h"),
"open": [1.0] * rows,
"high": [1.02] * rows,
"low": [0.98] * rows,
"close": [1.0] * rows,
"volume": [1000.0] * rows,
})
def test_theme_static_accumulation_bonus_can_upgrade_to_recommend():
event = {
"source": "coingecko_trending",
"symbol": "DOGS/USDT",
"title": "[主题扩散:ton_ecosystem] Telegram becomes the main driver of the TON ecosystem",
"importance": "A",
"event_type": "theme_expansion",
}
with patch.object(ed, "_ticker_info", return_value={"price": 0.001, "change_24h": 5.0, "volume_24h": 10000000}), \
patch.object(ed, "fetch_klines", return_value=_fake_ohlcv()), \
patch.object(ed, "detect_volume_price_fly", return_value=None), \
patch.object(ed, "detect_static_accumulation", return_value={"static_count": 23, "vol_ratio": 1.4}), \
patch.object(ed, "full_pa_analysis", return_value={"ignition_points": []}), \
patch.object(ed, "fetch_derivatives_context", return_value={"funding_rate": 0, "top_trader_long_pct": 56}), \
patch.object(ed, "calc_atr", return_value=0.00005):
result = ed.quick_technical_check(event)
assert result["score"] >= 6
assert result["decision"] == "recommend"
assert any("生态主题+强静K蓄力升权" in s for s in result["signals"])