242 lines
9.6 KiB
Python
242 lines
9.6 KiB
Python
import os
|
|
import sys
|
|
|
|
import pandas as pd
|
|
|
|
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.services import altcoin_screener
|
|
|
|
|
|
def test_fetch_all_tickers_filters_stable_and_fiat_suffixes(monkeypatch):
|
|
monkeypatch.setattr(
|
|
altcoin_screener.exchange,
|
|
"fetch_tickers",
|
|
lambda: {
|
|
"BTC/USDT": {"last": 1, "percentage": 1, "quoteVolume": 100},
|
|
"RLUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"BFUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"EUR/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"AI/USDT": {"last": 1, "percentage": 5, "quoteVolume": 1000},
|
|
"USD1/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"U/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"XUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"FRAX/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"LUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"GUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"SUSD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"USDD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"EURS/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
"AUD/USDT": {"last": 1, "percentage": 0.1, "quoteVolume": 100},
|
|
},
|
|
)
|
|
|
|
pairs = altcoin_screener.fetch_all_tickers()
|
|
|
|
assert "AI/USDT" in pairs
|
|
assert "RLUSD/USDT" not in pairs
|
|
assert "BFUSD/USDT" not in pairs
|
|
assert "EUR/USDT" not in pairs
|
|
assert "USD1/USDT" not in pairs
|
|
assert "U/USDT" not in pairs
|
|
assert "XUSD/USDT" not in pairs
|
|
assert "FRAX/USDT" not in pairs
|
|
assert "LUSD/USDT" not in pairs
|
|
assert "GUSD/USDT" not in pairs
|
|
assert "SUSD/USDT" not in pairs
|
|
assert "USDD/USDT" not in pairs
|
|
assert "EURS/USDT" not in pairs
|
|
assert "AUD/USDT" not in pairs
|
|
assert "BTC/USDT" not in pairs
|
|
|
|
|
|
def _mock_weights():
|
|
return {
|
|
"量价齐飞": 5,
|
|
"N倍放量": 5,
|
|
"连续3x放量": 4,
|
|
"布林收窄": 3,
|
|
"静K蓄力": 2,
|
|
"Q≥7供给区突破": 4,
|
|
"Q7供给区突破": 4,
|
|
"动K(阳)+量递增": 3,
|
|
"动K阳量递增": 3,
|
|
"连续K加速": 3,
|
|
"板块联动": 3,
|
|
"大户偏多": 1,
|
|
"静K→动K转折": 4,
|
|
"静K动K转折": 4,
|
|
"1H放量(量价背离)": 1,
|
|
}
|
|
|
|
|
|
def test_volume_price_fly_accepts_two_consecutive_4x_bars(monkeypatch):
|
|
monkeypatch.setattr(
|
|
altcoin_screener,
|
|
"vp_fly_params",
|
|
lambda: {"vol_ratio_min": 5.0, "body_ratio_min": 0.70, "consecutive_relaxed_vol_ratio_min": 4.0},
|
|
)
|
|
|
|
rows = []
|
|
for i in range(20):
|
|
rows.append({"open": 1.0, "high": 1.03, "low": 0.99, "close": 1.01, "volume": 100.0})
|
|
rows.extend([
|
|
{"open": 1.00, "high": 1.10, "low": 0.99, "close": 1.09, "volume": 650.0},
|
|
{"open": 1.09, "high": 1.20, "low": 1.08, "close": 1.18, "volume": 670.0},
|
|
])
|
|
df = pd.DataFrame(rows)
|
|
|
|
vp = altcoin_screener.detect_volume_price_fly(df)
|
|
|
|
assert vp["vp_fly_count"] == 2
|
|
assert len(vp["vp_fly_details"]) == 2
|
|
|
|
|
|
def test_layer1_keeps_high_momentum_breakout_without_5x_vp(monkeypatch):
|
|
monkeypatch.setattr(altcoin_screener, "fetch_all_tickers", lambda: {
|
|
"CREAM/USDT": {"price": 2.1, "change_24h": 12.0, "volume_24h": 8000000},
|
|
})
|
|
monkeypatch.setattr(altcoin_screener, "fetch_funding_rates", lambda: {})
|
|
monkeypatch.setattr(altcoin_screener, "is_meme_coin", lambda symbol: False)
|
|
monkeypatch.setattr(altcoin_screener, "get_burst_threshold", lambda symbol: 20)
|
|
monkeypatch.setattr(altcoin_screener, "funding_rate_params", lambda: {"long_extreme": 0.001, "short_extreme": -0.0005})
|
|
monkeypatch.setattr(altcoin_screener, "detect_bollinger_squeeze", lambda df: None)
|
|
monkeypatch.setattr(altcoin_screener, "vp_fly_params", lambda: {"vol_ratio_min": 5.0, "body_ratio_min": 0.70, "consecutive_relaxed_vol_ratio_min": 4.0})
|
|
monkeypatch.setattr(altcoin_screener, "get_dynamic_weights", lambda: {
|
|
"量价齐飞": 5,
|
|
"N倍放量(≥10x)": 6,
|
|
"连续3x放量(≥3根)": 4,
|
|
"布林收窄": 3,
|
|
"静K蓄力": 2,
|
|
"1H放量(量价背离)": 1,
|
|
})
|
|
|
|
h1_rows = []
|
|
for _ in range(20):
|
|
h1_rows.append([0, 1.0, 1.03, 0.99, 1.01, 100.0])
|
|
h1_rows.extend([
|
|
[0, 1.00, 1.10, 0.99, 1.09, 650.0],
|
|
[0, 1.09, 1.20, 1.08, 1.18, 670.0],
|
|
])
|
|
h4_rows = []
|
|
price = 1.0
|
|
for _ in range(30):
|
|
h4_rows.append([0, price, price * 1.01, price * 0.99, price * 1.002, 100.0])
|
|
price *= 1.001
|
|
|
|
def fake_fetch_klines(symbol, timeframe, limit=100):
|
|
data = h1_rows if timeframe == '1h' else h4_rows
|
|
return pd.DataFrame(data, columns=['timestamp','open','high','low','close','volume'])
|
|
|
|
monkeypatch.setattr(altcoin_screener, "fetch_klines", fake_fetch_klines)
|
|
|
|
candidates = altcoin_screener.layer1_coarse_filter()
|
|
vp = altcoin_screener.detect_volume_price_fly(fake_fetch_klines('CREAM/USDT', '1h'))
|
|
assert vp["vp_fly_count"] == 2
|
|
|
|
assert "CREAM/USDT" in candidates
|
|
assert any("量价齐飞" in s for s in candidates["CREAM/USDT"]["anomalies"])
|
|
assert any("连续2根量价齐飞K" in s for s in candidates["CREAM/USDT"]["anomalies"])
|
|
|
|
|
|
def test_static_accumulation_bypass_promotes_expired_to_accumulate(monkeypatch):
|
|
monkeypatch.setattr(altcoin_screener, "get_dynamic_weights", _mock_weights)
|
|
monkeypatch.setattr(altcoin_screener, "state_score_thresholds", lambda: (8, 10, 3))
|
|
monkeypatch.setattr(
|
|
altcoin_screener,
|
|
"get_screener_section",
|
|
lambda name=None: {
|
|
"sector_rotation": {
|
|
"bonus_weight": 0,
|
|
"min_non_sector_signals_for_accelerate": 2,
|
|
"sector_only_max_state": "蓄力",
|
|
},
|
|
"static_accumulation_bypass": {
|
|
"min_score": 2,
|
|
"min_vol_ratio": 1.0,
|
|
"min_static_count": 3,
|
|
},
|
|
}.get(name, {}),
|
|
)
|
|
monkeypatch.setattr(altcoin_screener, "fetch_top_trader_ratio", lambda symbol: None)
|
|
monkeypatch.setattr(altcoin_screener, "log_screening", lambda **kwargs: None)
|
|
monkeypatch.setattr(altcoin_screener, "create_recommendation", lambda **kwargs: 456)
|
|
monkeypatch.setattr(altcoin_screener, "get_sector_for_coin", lambda symbol: [])
|
|
monkeypatch.setattr(altcoin_screener, "dynamic_leader_detection", lambda perf: {})
|
|
monkeypatch.setattr(altcoin_screener, "SECTOR_MEMBERS", {})
|
|
|
|
qualified, _, _ = altcoin_screener.layer2_fine_filter({
|
|
"PNT/USDT": {
|
|
"anomaly_score": 2,
|
|
"price": 1.0,
|
|
"change_24h": 4.0,
|
|
"funding_rate": 0.0,
|
|
"is_meme": False,
|
|
"vp_data": None,
|
|
"bb_data": None,
|
|
"static_accumulation": {"static_count": 5, "vol_ratio": 1.1},
|
|
"h4_df": None,
|
|
}
|
|
})
|
|
|
|
assert qualified["PNT/USDT"]["state"] == "蓄力"
|
|
assert qualified["PNT/USDT"]["base_state"] == "过期"
|
|
assert qualified["PNT/USDT"]["force_reason"] == "静K蓄力旁路"
|
|
assert any("静K蓄力旁路入池" in s for s in qualified["PNT/USDT"]["signals"])
|
|
|
|
|
|
def test_strong_static_accumulation_can_promote_to_accelerate(monkeypatch):
|
|
monkeypatch.setattr(altcoin_screener, "get_dynamic_weights", _mock_weights)
|
|
monkeypatch.setattr(altcoin_screener, "state_score_thresholds", lambda: (8, 10, 3))
|
|
monkeypatch.setattr(
|
|
altcoin_screener,
|
|
"get_screener_section",
|
|
lambda name=None: {
|
|
"sector_rotation": {
|
|
"bonus_weight": 0,
|
|
"min_non_sector_signals_for_accelerate": 2,
|
|
"sector_only_max_state": "蓄力",
|
|
},
|
|
"static_accumulation_bypass": {
|
|
"min_score": 2,
|
|
"min_vol_ratio": 1.0,
|
|
"min_static_count": 3,
|
|
"direct_accelerate": {
|
|
"enabled": True,
|
|
"min_static_count": 10,
|
|
"min_vol_ratio": 1.25,
|
|
"min_score": 5,
|
|
},
|
|
},
|
|
}.get(name, {}),
|
|
)
|
|
monkeypatch.setattr(altcoin_screener, "fetch_top_trader_ratio", lambda symbol: None)
|
|
monkeypatch.setattr(altcoin_screener, "log_screening", lambda **kwargs: None)
|
|
created = []
|
|
monkeypatch.setattr(altcoin_screener, "create_recommendation", lambda **kwargs: created.append(kwargs) or 789)
|
|
monkeypatch.setattr(altcoin_screener, "get_sector_for_coin", lambda symbol: [])
|
|
monkeypatch.setattr(altcoin_screener, "dynamic_leader_detection", lambda perf: {})
|
|
monkeypatch.setattr(altcoin_screener, "SECTOR_MEMBERS", {})
|
|
|
|
qualified, _, _ = altcoin_screener.layer2_fine_filter({
|
|
"PNT/USDT": {
|
|
"anomaly_score": 5,
|
|
"price": 1.0,
|
|
"change_24h": 4.0,
|
|
"funding_rate": 0.0,
|
|
"is_meme": False,
|
|
"vp_data": None,
|
|
"bb_data": None,
|
|
"static_accumulation": {"static_count": 12, "vol_ratio": 1.4},
|
|
"h4_df": None,
|
|
}
|
|
})
|
|
|
|
assert qualified["PNT/USDT"]["state"] == "加速"
|
|
assert qualified["PNT/USDT"]["base_state"] == "蓄力"
|
|
assert qualified["PNT/USDT"]["force_reason"] == "强静K蓄力直升加速"
|
|
assert any("强静K蓄力直升加速" in s for s in qualified["PNT/USDT"]["signals"])
|
|
assert created and created[0]["force_reason"] == "强静K蓄力直升加速"
|