"""资金费率:当前 premiumIndex + 历史 fundingRate 存 SQLite。""" import logging import time from datetime import datetime from typing import Any from .binance import binance_client from .config import settings from .db import ( get_funding_history_from_db, get_funding_meta, save_funding_current_bulk, save_funding_history, ) from .exceptions import BinanceRateLimitedError logger = logging.getLogger(__name__) _premium_cache: dict[str, Any] = {} _premium_cache_at: float = 0.0 def _premium_ttl_sec() -> int: return settings.funding_cache_minutes * 60 async def get_current_funding_map(force: bool = False) -> dict[str, dict]: """全市场当前资金费率(一次 premiumIndex)。""" global _premium_cache, _premium_cache_at now = time.time() if not force and _premium_cache and now - _premium_cache_at < _premium_ttl_sec(): return _premium_cache if binance_client.is_rate_limited(): if _premium_cache: return _premium_cache raise BinanceRateLimitedError(binance_client.rate_limit_remaining_sec(), "premiumIndex") data = await binance_client.get_premium_index_all() _premium_cache = data _premium_cache_at = now save_funding_current_bulk(data) return data def _is_history_fresh(symbol: str, min_bars: int) -> bool: meta = get_funding_meta(symbol) if not meta or meta.get("bar_count", 0) < min_bars: return False try: last = datetime.fromisoformat(meta["last_fetch_at"]) except ValueError: return False return (datetime.now() - last).total_seconds() < _premium_ttl_sec() async def sync_funding_history(symbol: str, limit: int | None = None) -> list[dict]: sym = symbol.upper() n = min(limit or settings.funding_history_limit, 1000) rows = await binance_client.get_funding_rate_history(sym, n) try: cur_map = await get_current_funding_map() nft = int(cur_map.get(sym, {}).get("nextFundingTime", 0) or 0) except Exception: nft = 0 save_funding_history(sym, rows, next_funding_time=nft) logger.info("Saved %d funding records for %s", len(rows), sym) return rows async def get_funding_bundle( symbol: str, limit: int | None = None, force_refresh: bool = False, ) -> dict[str, Any]: sym = symbol.upper() n = min(limit or settings.funding_history_limit, 1000) min_bars = min(n, 10) current_map = await get_current_funding_map() cur = current_map.get(sym, {}) current = { "rate": float(cur.get("lastFundingRate", 0) or 0), "rate_pct": float(cur.get("lastFundingRate", 0) or 0) * 100, "next_funding_time": int(cur.get("nextFundingTime", 0) or 0), "mark_price": float(cur.get("markPrice", 0) or 0), } if not force_refresh and _is_history_fresh(sym, min_bars): history = get_funding_history_from_db(sym, n) if len(history) >= min_bars: return { "symbol": sym, "current": current, "history": history, "source": "db", } stored = get_funding_history_from_db(sym, n) if binance_client.is_rate_limited(): if stored: return {"symbol": sym, "current": current, "history": stored, "source": "db_stale"} raise BinanceRateLimitedError(binance_client.rate_limit_remaining_sec(), sym) try: history = await sync_funding_history(sym, n) return {"symbol": sym, "current": current, "history": history, "source": "binance"} except BinanceRateLimitedError: if stored: return {"symbol": sym, "current": current, "history": stored, "source": "db_stale"} raise except Exception: if stored: return {"symbol": sym, "current": current, "history": stored, "source": "db_stale"} raise async def enrich_items_with_funding(items: list[dict]) -> list[dict]: try: current_map = await get_current_funding_map() except Exception as e: logger.warning("Funding current map failed: %s", e) current_map = {} for item in items: sym = item.get("symbol", "") info = current_map.get(sym, {}) rate = float(info.get("lastFundingRate", 0) or 0) item["funding_rate"] = rate item["funding_rate_pct"] = rate * 100 item["funding_rate_fmt"] = f"{rate * 100:.4f}%" nft = info.get("nextFundingTime") item["next_funding_time"] = int(nft) if nft else None return items async def prefetch_funding(symbols: list[str]) -> None: seen: set[str] = set() try: await get_current_funding_map() except Exception as e: logger.warning("Prefetch premiumIndex failed: %s", e) for raw in symbols: sym = raw.upper().strip() if not sym or sym in seen: continue seen.add(sym) if _is_history_fresh(sym, 10): continue if binance_client.is_rate_limited(): logger.warning("Prefetch funding stopped — rate limited") break try: await sync_funding_history(sym) except BinanceRateLimitedError: break except Exception as e: logger.warning("Prefetch funding %s failed: %s", sym, e)