diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 1cf9f98..0e43966 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -268,7 +268,9 @@ ACCOUNT_BALANCE_CACHE = { } LIQUIDITY_RANK_CACHE = { "updated_at": 0.0, + "version": 0, "ranks": {}, + "volumes": {}, "total": 0, } @@ -3165,13 +3167,36 @@ def _status_by_ema55(symbol, timeframe): return "横盘", None, None -def _daily_volume_rank(symbol): +def _okx_usdt_swap_volume_by_base() -> dict[str, float]: """ - 返回(symbol_rank, total_count):OKX USDT 永续 24h 成交额(USDT) 在全市场币种中的排名(非「本月」)。 - 同一 base 多合约取成交额最大的一条;不用 vol24h(张数)参与排序。 + 与 OKX 网页「24小时成交额」一致:走官方 /market/tickers?instType=SWAP 的 volCcy24h(USDT)。 + ccxt 的 quoteVolume 在 OKX 上常不可靠,会导致 ZEC 等币种排名严重偏低。 """ - sym_norm = normalize_symbol_input(symbol) - target_base = journal_coin_from_symbol(sym_norm) + by_base: dict[str, float] = {} + try: + ensure_markets_loaded() + if hasattr(exchange, "publicGetMarketTickers"): + resp = exchange.publicGetMarketTickers({"instType": "SWAP"}) + rows = (resp or {}).get("data") or [] + for row in rows: + if not isinstance(row, dict): + continue + inst = str(row.get("instId") or "").upper() + # 仅 USDT 本位永续:BTC-USDT-SWAP + parts = inst.split("-") + if len(parts) < 3 or parts[-1] != "SWAP" or parts[1] != "USDT": + continue + base = parts[0].strip() + if not base: + continue + qv = _safe_float(row.get("volCcy24h")) + if qv is None or qv <= 0: + continue + by_base[base] = max(by_base.get(base, 0), float(qv)) + if by_base: + return by_base + except Exception: + pass def _ticker_base(sym_text): s = str(sym_text or "").upper().strip() @@ -3184,57 +3209,69 @@ def _daily_volume_rank(symbol): if s.endswith("USDT"): return s[:-4].strip() return s + + try: + ensure_markets_loaded() + try: + tickers = exchange.fetch_tickers(params={"instType": "SWAP"}) + except Exception: + tickers = exchange.fetch_tickers() + for s, t in (tickers or {}).items(): + try: + mk = exchange.markets.get(s) if exchange.markets else None + if mk is not None: + if not mk.get("swap"): + continue + if str(mk.get("quote") or "").upper() != "USDT": + continue + if mk.get("linear") is False: + continue + if mk.get("active") is False: + continue + else: + su = str(s).upper() + if "USDT" not in su or ":USDT" not in su: + continue + base = _ticker_base(s) + if not base: + continue + info = (t or {}).get("info") if isinstance((t or {}).get("info"), dict) else {} + qv = _safe_float(info.get("volCcy24h")) + if qv is None: + qv = _safe_float((t or {}).get("quoteVolume")) + if qv is None or qv <= 0: + continue + by_base[base] = max(by_base.get(base, 0), float(qv)) + except Exception: + continue + except Exception: + pass + return by_base + + +def _daily_volume_rank(symbol): + """ + 返回(symbol_rank, total_count):OKX USDT 永续 24h 成交额(USDT) 在全市场币种中的排名(非「本月」)。 + 数据与 OKX App「24小时成交额」同源字段 volCcy24h。 + """ + sym_norm = normalize_symbol_input(symbol) + target_base = journal_coin_from_symbol(sym_norm) now_ts = time.time() + cache_ver = 3 cached_ok = ( - LIQUIDITY_RANK_CACHE["updated_at"] + LIQUIDITY_RANK_CACHE.get("version") == cache_ver + and LIQUIDITY_RANK_CACHE["updated_at"] and now_ts - float(LIQUIDITY_RANK_CACHE["updated_at"]) < max(30, BALANCE_REFRESH_SECONDS) ) if not cached_ok: try: - ensure_markets_loaded() - try: - tickers = exchange.fetch_tickers(params={"instType": "SWAP"}) - except Exception: - tickers = exchange.fetch_tickers() - by_base: dict[str, float] = {} - for s, t in (tickers or {}).items(): - try: - mk = exchange.markets.get(s) if exchange.markets else None - if mk is not None: - if not mk.get("swap"): - continue - if str(mk.get("quote") or "").upper() != "USDT": - continue - if mk.get("active") is False: - continue - else: - su = str(s).upper() - if "USDT" not in su or ":USDT" not in su: - continue - base = _ticker_base(s) - if not base: - continue - qv = _safe_float((t or {}).get("quoteVolume")) - if qv is None: - info = (t or {}).get("info") if isinstance((t or {}).get("info"), dict) else {} - # OKX:volCcy24h = 24h 成交额(USDT);vol24h 多为张数,不能参与排名 - qv = _safe_float(info.get("volCcy24h")) - if qv is None: - bv = _safe_float((t or {}).get("baseVolume")) - lp = _safe_float((t or {}).get("last")) - if bv is not None and lp is not None: - qv = bv * lp - if qv is None or qv <= 0: - continue - prev = by_base.get(base) - if prev is None or float(qv) > prev: - by_base[base] = float(qv) - except Exception: - continue + by_base = _okx_usdt_swap_volume_by_base() scored = sorted(by_base.items(), key=lambda x: x[1], reverse=True) ranks = {base: idx for idx, (base, _) in enumerate(scored, 1)} LIQUIDITY_RANK_CACHE["ranks"] = ranks + LIQUIDITY_RANK_CACHE["volumes"] = dict(by_base) LIQUIDITY_RANK_CACHE["total"] = len(scored) + LIQUIDITY_RANK_CACHE["version"] = cache_ver LIQUIDITY_RANK_CACHE["updated_at"] = now_ts except Exception: pass @@ -4910,8 +4947,10 @@ def api_symbol_liquidity_rank(): if not symbol: return jsonify({"ok": False, "msg": "symbol 不能为空"}), 400 rank, total = _daily_volume_rank(symbol) + base = journal_coin_from_symbol(symbol) + vol_24h = (LIQUIDITY_RANK_CACHE.get("volumes") or {}).get(base) if total <= 0: - return jsonify({"ok": False, "msg": "日成交量排名读取失败"}), 502 + return jsonify({"ok": False, "msg": "24h成交额排名读取失败"}), 502 if rank is None: return jsonify( { @@ -4919,6 +4958,7 @@ def api_symbol_liquidity_rank(): "symbol": symbol, "rank": None, "total": int(total), + "vol_usdt_24h": vol_24h, "in_top30": False, "rank_max": KEY_DAILY_VOLUME_RANK_MAX, } @@ -4930,6 +4970,7 @@ def api_symbol_liquidity_rank(): "symbol": symbol, "rank": int(rank), "total": int(total), + "vol_usdt_24h": vol_24h, "in_top30": in_top, "in_top": in_top, "rank_max": KEY_DAILY_VOLUME_RANK_MAX, diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index e355c76..fbcb715 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -1461,7 +1461,7 @@ if(keyForm){ const rankMax = data.rank_max || 30; const inTop = data.in_top != null ? data.in_top : data.in_top30; if(data.rank == null || !inTop){ - alert(`${data.symbol} 当前日成交量排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`); + alert(`${data.symbol} 当前24h成交额排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截。`); return; } keyForm.submit();