alphax/tests/test_event_driven_screener.py
2026-05-25 21:57:38 +08:00

247 lines
8.7 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 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_theme_expansion_respects_max_expanded_symbols(monkeypatch):
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": {},
}
original_cfg = ed._cfg()
limited_cfg = json.loads(json.dumps(original_cfg))
limited_cfg["theme_expansion"]["max_expanded_symbols"] = 2
monkeypatch.setattr(ed, "_cfg", lambda: limited_cfg)
expanded = ed.expand_theme_events([event])
assert len(expanded) <= 3 # 原始事件 + 最多 2 个扩散事件
def test_wublock_atom_feed_events_are_parsed_and_symbolized(monkeypatch):
xml = """<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<title>SUI ecosystem project announces major upgrade and airdrop</title>
<link href="https://www.wublock123.com/example"/>
<updated>2026-05-16T10:00:00Z</updated>
</entry>
</feed>
"""
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 = """<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<item>
<title>Solana 生态 Meme 项目成交量大幅上升SUI 同步活跃</title>
<link>https://www.panewslab.com/example</link>
<pubDate>Sat, 16 May 2026 10:00:00 GMT</pubDate>
</item>
</channel>
</rss>
"""
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"])
def test_run_once_caps_processed_events(monkeypatch):
events = [
{
"source": "unit",
"symbol": f"T{i}/USDT",
"title": f"event {i}",
"url": "",
"published_at": datetime.now(),
"importance": "A",
"event_type": "news",
"event_hash": f"h{i}",
"raw": {},
}
for i in range(6)
]
processed = []
monkeypatch.setattr(ed, "init_db", lambda: None)
monkeypatch.setattr(ed, "init_event_tables", lambda: None)
monkeypatch.setattr(ed, "collect_events", lambda: events)
monkeypatch.setattr(ed, "store_events", lambda collected: collected)
monkeypatch.setattr(ed, "process_event", lambda event: processed.append(event["symbol"]) or {
"event": event,
"result": {"decision": "ignore", "score": 0, "reason": ""},
"rec_id": 0,
"pushed": False,
})
monkeypatch.setattr(ed, "log_cron_run", lambda **kwargs: None)
result = ed.run_once(limit=2, max_seconds=30)
assert result["processed_count"] == 2
assert result["skipped_due_to_limit"] == 4
assert processed == ["T0/USDT", "T1/USDT"]