refactor: 将共用代码迁入 lib/ 模块化目录
统一 strategy、key_monitor、trade、hub 等共用库到 lib/ 子包,并补充 lib-structure 文档,便于四所与中控维护。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+184
-184
@@ -1,184 +1,184 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from hub_volume_rank_lib import (
|
||||
CACHE_VERSION,
|
||||
LIQUIDITY_RANK_CACHE_VERSION,
|
||||
TOP_N_DEFAULT,
|
||||
_exchange_rank_row_stale,
|
||||
_okx_turnover_usdt,
|
||||
_scores_from_binance,
|
||||
_scores_from_gate,
|
||||
build_usdt_swap_volume_ranks,
|
||||
cache_needs_refresh,
|
||||
format_volume_quote,
|
||||
merge_exchange_rank,
|
||||
rank_date_label,
|
||||
resolve_daily_volume_rank,
|
||||
)
|
||||
|
||||
|
||||
def test_rank_date_label_after_reset():
|
||||
# 2026-06-08 09:00 北京时间 → 昨日交易日 2026-06-07
|
||||
dt = datetime(2026, 6, 8, 9, 0, 0)
|
||||
assert rank_date_label(now=dt, reset_hour=8) == "2026-06-07"
|
||||
|
||||
|
||||
def test_rank_date_label_before_reset():
|
||||
# 2026-06-08 07:00 → 当前交易日仍算 2026-06-07,昨日为 2026-06-06
|
||||
dt = datetime(2026, 6, 8, 7, 0, 0)
|
||||
assert rank_date_label(now=dt, reset_hour=8) == "2026-06-06"
|
||||
|
||||
|
||||
def test_format_volume_quote():
|
||||
assert format_volume_quote(1_500_000_000) == "1.50B"
|
||||
assert format_volume_quote(2_300_000) == "2.30M"
|
||||
assert format_volume_quote(4500) == "4.50K"
|
||||
|
||||
|
||||
def test_okx_turnover_usdt():
|
||||
qv = _okx_turnover_usdt({"volCcy24h": "100", "last": "50"})
|
||||
assert qv == 5000.0
|
||||
|
||||
|
||||
def test_cache_needs_refresh_and_merge():
|
||||
cache = {"rank_date": "2026-06-05", "exchanges": {}}
|
||||
assert cache_needs_refresh(cache, expected_rank_date="2026-06-07") is True
|
||||
merged = merge_exchange_rank(
|
||||
cache,
|
||||
"binance",
|
||||
{
|
||||
"ok": True,
|
||||
"rank_date": "2026-06-07",
|
||||
"items": [{"rank": 1, "symbol": "BTC/USDT", "volume_quote": 1.0}],
|
||||
"total_symbols": 100,
|
||||
},
|
||||
)
|
||||
assert merged["exchanges"]["binance"]["items"][0]["symbol"] == "BTC/USDT"
|
||||
assert merged["rank_date"] == "2026-06-07"
|
||||
|
||||
|
||||
def test_stale_cache_version_forces_refresh():
|
||||
cache = {"version": CACHE_VERSION - 1, "rank_date": "2026-06-07", "exchanges": {"okx": {"items": [{}]}}}
|
||||
assert cache_needs_refresh(cache) is True
|
||||
|
||||
|
||||
def test_short_item_list_is_stale():
|
||||
items = [{"rank": i, "symbol": f"S{i}/USDT"} for i in range(1, 13)]
|
||||
row = {"items": items, "total_symbols": 12}
|
||||
assert _exchange_rank_row_stale(row) is True
|
||||
full = {"items": items + [{"rank": i, "symbol": f"X{i}/USDT"} for i in range(13, TOP_N_DEFAULT + 1)], "total_symbols": 300}
|
||||
assert _exchange_rank_row_stale(full) is False
|
||||
|
||||
|
||||
def test_scores_from_binance_uses_fapi_lightweight_api():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "BTCUSDT", "quoteVolume": "9000000"},
|
||||
{"symbol": "ETHUSDT", "quoteVolume": "5000000"},
|
||||
]
|
||||
scored = _scores_from_binance(ex)
|
||||
assert scored[0][1] == "BTC"
|
||||
assert scored[0][2] == 9000000.0
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_binance_skips_fetch_tickers_on_api_error():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.side_effect = RuntimeError("network")
|
||||
scored = _scores_from_binance(ex)
|
||||
assert scored == []
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_gate_uses_futures_tickers_api():
|
||||
ex = MagicMock()
|
||||
ex.id = "gateio"
|
||||
ex.publicFuturesGetSettleTickers.return_value = [
|
||||
{"contract": "BTC_USDT", "volume_24h_quote": "8000000"},
|
||||
{"contract": "ETH_USDT", "volume_24h_quote": "4000000"},
|
||||
]
|
||||
scored = _scores_from_gate(ex)
|
||||
assert scored[0][1] == "BTC"
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_gate_skips_fetch_tickers_on_api_error():
|
||||
ex = MagicMock()
|
||||
ex.id = "gateio"
|
||||
ex.publicFuturesGetSettleTickers.side_effect = RuntimeError("network")
|
||||
scored = _scores_from_gate(ex)
|
||||
assert scored == []
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_resolve_daily_volume_rank_caches_result():
|
||||
cache = {"version": 0, "updated_at": 0.0, "ranks": {}, "total": 0}
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "BTCUSDT", "quoteVolume": "100"},
|
||||
{"symbol": "ETHUSDT", "quoteVolume": "50"},
|
||||
]
|
||||
|
||||
rank, total = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=1000.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank == 1
|
||||
assert total == 2
|
||||
assert cache["version"] == LIQUIDITY_RANK_CACHE_VERSION
|
||||
calls = ex.fapiPublicGetTicker24hr.call_count
|
||||
|
||||
rank2, _ = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=1010.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank2 == 1
|
||||
assert ex.fapiPublicGetTicker24hr.call_count == calls
|
||||
|
||||
|
||||
def test_resolve_daily_volume_rank_keeps_stale_cache_when_refresh_empty():
|
||||
cache = {
|
||||
"version": LIQUIDITY_RANK_CACHE_VERSION,
|
||||
"updated_at": 900.0,
|
||||
"ranks": {"BTC": 1},
|
||||
"total": 100,
|
||||
}
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = []
|
||||
|
||||
rank, total = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=2000.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank == 1
|
||||
assert total == 100
|
||||
assert cache["updated_at"] == 900.0
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_build_usdt_swap_volume_ranks():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "SOLUSDT", "quoteVolume": "200"},
|
||||
]
|
||||
ranks, total = build_usdt_swap_volume_ranks(ex, lambda: None)
|
||||
assert ranks["SOL"] == 1
|
||||
assert total == 1
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from lib.hub.hub_volume_rank_lib import (
|
||||
CACHE_VERSION,
|
||||
LIQUIDITY_RANK_CACHE_VERSION,
|
||||
TOP_N_DEFAULT,
|
||||
_exchange_rank_row_stale,
|
||||
_okx_turnover_usdt,
|
||||
_scores_from_binance,
|
||||
_scores_from_gate,
|
||||
build_usdt_swap_volume_ranks,
|
||||
cache_needs_refresh,
|
||||
format_volume_quote,
|
||||
merge_exchange_rank,
|
||||
rank_date_label,
|
||||
resolve_daily_volume_rank,
|
||||
)
|
||||
|
||||
|
||||
def test_rank_date_label_after_reset():
|
||||
# 2026-06-08 09:00 北京时间 → 昨日交易日 2026-06-07
|
||||
dt = datetime(2026, 6, 8, 9, 0, 0)
|
||||
assert rank_date_label(now=dt, reset_hour=8) == "2026-06-07"
|
||||
|
||||
|
||||
def test_rank_date_label_before_reset():
|
||||
# 2026-06-08 07:00 → 当前交易日仍算 2026-06-07,昨日为 2026-06-06
|
||||
dt = datetime(2026, 6, 8, 7, 0, 0)
|
||||
assert rank_date_label(now=dt, reset_hour=8) == "2026-06-06"
|
||||
|
||||
|
||||
def test_format_volume_quote():
|
||||
assert format_volume_quote(1_500_000_000) == "1.50B"
|
||||
assert format_volume_quote(2_300_000) == "2.30M"
|
||||
assert format_volume_quote(4500) == "4.50K"
|
||||
|
||||
|
||||
def test_okx_turnover_usdt():
|
||||
qv = _okx_turnover_usdt({"volCcy24h": "100", "last": "50"})
|
||||
assert qv == 5000.0
|
||||
|
||||
|
||||
def test_cache_needs_refresh_and_merge():
|
||||
cache = {"rank_date": "2026-06-05", "exchanges": {}}
|
||||
assert cache_needs_refresh(cache, expected_rank_date="2026-06-07") is True
|
||||
merged = merge_exchange_rank(
|
||||
cache,
|
||||
"binance",
|
||||
{
|
||||
"ok": True,
|
||||
"rank_date": "2026-06-07",
|
||||
"items": [{"rank": 1, "symbol": "BTC/USDT", "volume_quote": 1.0}],
|
||||
"total_symbols": 100,
|
||||
},
|
||||
)
|
||||
assert merged["exchanges"]["binance"]["items"][0]["symbol"] == "BTC/USDT"
|
||||
assert merged["rank_date"] == "2026-06-07"
|
||||
|
||||
|
||||
def test_stale_cache_version_forces_refresh():
|
||||
cache = {"version": CACHE_VERSION - 1, "rank_date": "2026-06-07", "exchanges": {"okx": {"items": [{}]}}}
|
||||
assert cache_needs_refresh(cache) is True
|
||||
|
||||
|
||||
def test_short_item_list_is_stale():
|
||||
items = [{"rank": i, "symbol": f"S{i}/USDT"} for i in range(1, 13)]
|
||||
row = {"items": items, "total_symbols": 12}
|
||||
assert _exchange_rank_row_stale(row) is True
|
||||
full = {"items": items + [{"rank": i, "symbol": f"X{i}/USDT"} for i in range(13, TOP_N_DEFAULT + 1)], "total_symbols": 300}
|
||||
assert _exchange_rank_row_stale(full) is False
|
||||
|
||||
|
||||
def test_scores_from_binance_uses_fapi_lightweight_api():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "BTCUSDT", "quoteVolume": "9000000"},
|
||||
{"symbol": "ETHUSDT", "quoteVolume": "5000000"},
|
||||
]
|
||||
scored = _scores_from_binance(ex)
|
||||
assert scored[0][1] == "BTC"
|
||||
assert scored[0][2] == 9000000.0
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_binance_skips_fetch_tickers_on_api_error():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.side_effect = RuntimeError("network")
|
||||
scored = _scores_from_binance(ex)
|
||||
assert scored == []
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_gate_uses_futures_tickers_api():
|
||||
ex = MagicMock()
|
||||
ex.id = "gateio"
|
||||
ex.publicFuturesGetSettleTickers.return_value = [
|
||||
{"contract": "BTC_USDT", "volume_24h_quote": "8000000"},
|
||||
{"contract": "ETH_USDT", "volume_24h_quote": "4000000"},
|
||||
]
|
||||
scored = _scores_from_gate(ex)
|
||||
assert scored[0][1] == "BTC"
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_scores_from_gate_skips_fetch_tickers_on_api_error():
|
||||
ex = MagicMock()
|
||||
ex.id = "gateio"
|
||||
ex.publicFuturesGetSettleTickers.side_effect = RuntimeError("network")
|
||||
scored = _scores_from_gate(ex)
|
||||
assert scored == []
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_resolve_daily_volume_rank_caches_result():
|
||||
cache = {"version": 0, "updated_at": 0.0, "ranks": {}, "total": 0}
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "BTCUSDT", "quoteVolume": "100"},
|
||||
{"symbol": "ETHUSDT", "quoteVolume": "50"},
|
||||
]
|
||||
|
||||
rank, total = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=1000.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank == 1
|
||||
assert total == 2
|
||||
assert cache["version"] == LIQUIDITY_RANK_CACHE_VERSION
|
||||
calls = ex.fapiPublicGetTicker24hr.call_count
|
||||
|
||||
rank2, _ = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=1010.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank2 == 1
|
||||
assert ex.fapiPublicGetTicker24hr.call_count == calls
|
||||
|
||||
|
||||
def test_resolve_daily_volume_rank_keeps_stale_cache_when_refresh_empty():
|
||||
cache = {
|
||||
"version": LIQUIDITY_RANK_CACHE_VERSION,
|
||||
"updated_at": 900.0,
|
||||
"ranks": {"BTC": 1},
|
||||
"total": 100,
|
||||
}
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = []
|
||||
|
||||
rank, total = resolve_daily_volume_rank(
|
||||
"BTC",
|
||||
cache,
|
||||
now_ts=2000.0,
|
||||
ttl_sec=60.0,
|
||||
exchange=ex,
|
||||
ensure_markets_loaded=lambda: None,
|
||||
)
|
||||
assert rank == 1
|
||||
assert total == 100
|
||||
assert cache["updated_at"] == 900.0
|
||||
ex.fetch_tickers.assert_not_called()
|
||||
|
||||
|
||||
def test_build_usdt_swap_volume_ranks():
|
||||
ex = MagicMock()
|
||||
ex.id = "binance"
|
||||
ex.fapiPublicGetTicker24hr.return_value = [
|
||||
{"symbol": "SOLUSDT", "quoteVolume": "200"},
|
||||
]
|
||||
ranks, total = build_usdt_swap_volume_ranks(ex, lambda: None)
|
||||
assert ranks["SOL"] == 1
|
||||
assert total == 1
|
||||
|
||||
Reference in New Issue
Block a user