首次上传
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import GateConfig
|
||||
from .proxy_util import httpx_proxy_url
|
||||
|
||||
|
||||
def _to_gate_interval(bar: str) -> str:
|
||||
b = (bar or "").strip()
|
||||
mapping = {
|
||||
"1m": "1m",
|
||||
"3m": "3m",
|
||||
"5m": "5m",
|
||||
"15m": "15m",
|
||||
"30m": "30m",
|
||||
"1H": "1h",
|
||||
"2H": "2h",
|
||||
"4H": "4h",
|
||||
"6H": "6h",
|
||||
"8H": "8h",
|
||||
"12H": "12h",
|
||||
"1D": "1d",
|
||||
"1W": "7d",
|
||||
"1M": "1M",
|
||||
}
|
||||
if b in mapping:
|
||||
return mapping[b]
|
||||
if len(b) >= 2 and b.endswith("H") and b[:-1].isdigit():
|
||||
return f"{b[:-1]}h"
|
||||
if len(b) >= 2 and b.endswith("D"):
|
||||
return b[:-1] + "d"
|
||||
return b.lower()
|
||||
|
||||
|
||||
def _candle_row(obj: dict[str, Any]) -> list[str]:
|
||||
ts_ms = str(int(float(obj["t"])) * 1000)
|
||||
o = str(obj.get("o") or "")
|
||||
h = str(obj.get("h") or "")
|
||||
l = str(obj.get("l") or "")
|
||||
c = str(obj.get("c") or "")
|
||||
v = str(obj.get("v") or "")
|
||||
sum_q = str(obj.get("sum") or "")
|
||||
return [ts_ms, o, h, l, c, v, v, sum_q, "1"]
|
||||
|
||||
|
||||
def _is_linear_usdt_perp_contract(item: dict[str, Any]) -> bool:
|
||||
name = str(item.get("name") or "")
|
||||
parts = name.split("_")
|
||||
if len(parts) != 2 or parts[1].upper() != "USDT":
|
||||
return False
|
||||
if item.get("in_delisting") is True:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class GateClient:
|
||||
"""Gate.io USDT 结算永续合约公共行情(REST v4)。"""
|
||||
|
||||
def __init__(self, conf: GateConfig, proxy_url: str | None = None) -> None:
|
||||
self.conf = conf
|
||||
self._proxy = httpx_proxy_url(proxy_url.strip() if proxy_url and str(proxy_url).strip() else None)
|
||||
self.timeout = httpx.Timeout(10.0, read=16.0)
|
||||
self._candle_sem = asyncio.Semaphore(3)
|
||||
|
||||
def _base_url(self) -> str:
|
||||
return str(self.conf.api_base).rstrip("/")
|
||||
|
||||
def _futures_prefix(self) -> str:
|
||||
return f"{self._base_url()}/futures/{self.conf.settle.strip().lower()}"
|
||||
|
||||
def _client_kwargs(self, timeout: httpx.Timeout) -> dict:
|
||||
if self._proxy:
|
||||
return {"timeout": timeout, "proxy": self._proxy, "trust_env": False}
|
||||
return {"timeout": timeout, "trust_env": True}
|
||||
|
||||
def symbol_to_swap_inst_id(self, symbol: str) -> str:
|
||||
base = symbol.strip().upper()
|
||||
return f"{base}_{self.conf.quote_currency.upper()}"
|
||||
|
||||
def inst_id_to_base_symbol(self, inst_id: str) -> str:
|
||||
inst = inst_id.strip().upper()
|
||||
suf = f"_{self.conf.quote_currency.upper()}"
|
||||
if inst.endswith(suf):
|
||||
return inst[: -len(suf)]
|
||||
return inst.split("_")[0].upper() if "_" in inst else inst
|
||||
|
||||
async def _fetch_contracts(self) -> list[dict[str, Any]]:
|
||||
url = f"{self._futures_prefix()}/contracts"
|
||||
async with httpx.AsyncClient(**self._client_kwargs(self.timeout)) as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not isinstance(data, list):
|
||||
raise RuntimeError(f"Gate contracts unexpected payload: {type(data)}")
|
||||
return data
|
||||
|
||||
async def list_live_usdt_swap_inst_ids(self) -> list[str]:
|
||||
"""全部 USDT 本位线性永续合约名(如 BTC_USDT),剔除交割/下架中的条目。"""
|
||||
data = await self._fetch_contracts()
|
||||
out: list[str] = []
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if not _is_linear_usdt_perp_contract(item):
|
||||
continue
|
||||
name = str(item.get("name") or "").strip()
|
||||
if name:
|
||||
out.append(name)
|
||||
return sorted(set(out))
|
||||
|
||||
async def get_perpetual_symbols(self) -> set[str]:
|
||||
ids = await self.list_live_usdt_swap_inst_ids()
|
||||
return {self.inst_id_to_base_symbol(i) for i in ids}
|
||||
|
||||
async def get_candles(self, inst_id: str, bar: str, limit: int = 120) -> list[list[str]]:
|
||||
"""
|
||||
返回按时间正序排列的 K 线列表(与旧 OKX 行格式对齐便于下游逻辑):
|
||||
[ts_ms, o, h, l, c, vol, vol_dup, sum_quote, confirm]
|
||||
"""
|
||||
interval = _to_gate_interval(bar)
|
||||
lim = max(1, min(int(limit), 2000))
|
||||
url = f"{self._futures_prefix()}/candlesticks"
|
||||
params = {"contract": inst_id, "interval": interval, "limit": str(lim)}
|
||||
async with self._candle_sem:
|
||||
await asyncio.sleep(0.12)
|
||||
async with httpx.AsyncClient(**self._client_kwargs(self.timeout)) as client:
|
||||
resp = await client.get(url, params=params)
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
if not isinstance(payload, list):
|
||||
raise RuntimeError(f"Gate candlesticks error: {payload}")
|
||||
rows: list[list[str]] = []
|
||||
for item in payload:
|
||||
if isinstance(item, dict) and "t" in item:
|
||||
rows.append(_candle_row(item))
|
||||
rows.sort(key=lambda r: int(r[0]) if r and r[0].isdigit() else 0)
|
||||
return rows
|
||||
|
||||
async def get_usdt_swap_est_quote_volume_map(self) -> dict[str, float]:
|
||||
"""
|
||||
合约名 -> 近 24h 计价币种成交额(USDT)。
|
||||
优先使用 ticker 的 volume_24h_quote;缺失时再尝试简单估算。
|
||||
"""
|
||||
url = f"{self._futures_prefix()}/tickers"
|
||||
tick_timeout = httpx.Timeout(15.0, read=90.0)
|
||||
async with httpx.AsyncClient(**self._client_kwargs(tick_timeout)) as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
payload = resp.json()
|
||||
if not isinstance(payload, list):
|
||||
raise RuntimeError(f"Gate tickers error: {type(payload)}")
|
||||
out: dict[str, float] = {}
|
||||
for item in payload:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
contract = str(item.get("contract") or "").strip()
|
||||
if not contract.endswith("_USDT"):
|
||||
continue
|
||||
vol_quote = item.get("volume_24h_quote") or item.get("volume_24h_usd")
|
||||
try:
|
||||
if vol_quote is not None and str(vol_quote).strip():
|
||||
out[contract] = max(0.0, float(vol_quote))
|
||||
continue
|
||||
last = float(item.get("last") or 0)
|
||||
vol_base = float(item.get("volume_24h_base") or item.get("volume_24h") or 0)
|
||||
out[contract] = max(0.0, vol_base * last)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return out
|
||||
Reference in New Issue
Block a user