交易所 API 改为仅服务器配置:前端去掉密钥、新机示例为空,并防止坏钥反复请求触发 Gate 封 IP。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-25 12:21:43 +08:00
parent f675f9997a
commit 75f50fe083
15 changed files with 350 additions and 60 deletions
+72 -13
View File
@@ -37,6 +37,11 @@ from lib.hub.hub_position_metrics import (
parse_position_unrealized_pnl,
resolve_position_display_upnl,
)
from lib.exchange.api_credentials_lib import (
is_exchange_auth_error,
normalize_api_credential,
strip_ccxt_credentials,
)
import ccxt
from fastapi import FastAPI, Header, HTTPException, Request
@@ -86,12 +91,31 @@ GATE_POS_MODE = "hedge" if _gate_pos in ("hedge", "dual", "double") else "single
app = FastAPI(title="sub-agent", docs_url=None, redoc_url=None)
_ccxt_ex: Any = None
_markets_loaded = False
# 鉴权失败冷却:中控会轮询 /status;坏钥时若持续签名请求,Gate 易封 IP
_AUTH_FAIL_UNTIL = 0.0
_AUTH_FAIL_MSG = ""
_AUTH_COOLDOWN_SEC = 600
def _socks_proxy_url(prefix: str) -> str:
return (os.getenv(f"{prefix}_SOCKS_PROXY") or "").strip()
def _raise_if_auth_cooling() -> None:
if time.time() < _AUTH_FAIL_UNTIL and _AUTH_FAIL_MSG:
raise RuntimeError(_AUTH_FAIL_MSG)
def _mark_auth_failure(exc: BaseException) -> None:
global _AUTH_FAIL_UNTIL, _AUTH_FAIL_MSG, _ccxt_ex, _markets_loaded
_AUTH_FAIL_MSG = f"交易所鉴权失败(已暂停签名请求 {_AUTH_COOLDOWN_SEC}s,避免封 IP): {exc}"
_AUTH_FAIL_UNTIL = time.time() + _AUTH_COOLDOWN_SEC
if _ccxt_ex is not None:
strip_ccxt_credentials(_ccxt_ex)
_ccxt_ex = None
_markets_loaded = False
def _http_https_proxy(prefix: str) -> dict[str, str] | None:
http = (os.getenv(f"{prefix}_HTTP_PROXY") or "").strip()
https = (os.getenv(f"{prefix}_HTTPS_PROXY") or "").strip()
@@ -110,11 +134,12 @@ def _attach_proxies(ex: Any, prefix: str) -> None:
def _make_exchange() -> Any:
_raise_if_auth_cooling()
if EXCHANGE_KIND == "binance":
key = (os.getenv("BINANCE_API_KEY") or "").strip()
secret = (os.getenv("BINANCE_API_SECRET") or "").strip()
key = normalize_api_credential(os.getenv("BINANCE_API_KEY"))
secret = normalize_api_credential(os.getenv("BINANCE_API_SECRET"))
if not key or not secret:
raise RuntimeError("缺少 BINANCE_API_KEY / BINANCE_API_SECRET")
raise RuntimeError("缺少 BINANCE_API_KEY / BINANCE_API_SECRET(请在服务器 .env 配置真密钥)")
ex = ccxt.binance(
{
"apiKey": key,
@@ -133,11 +158,11 @@ def _make_exchange() -> Any:
return ex
if EXCHANGE_KIND == "okx":
key = (os.getenv("OKX_API_KEY") or "").strip()
secret = (os.getenv("OKX_API_SECRET") or "").strip()
password = (os.getenv("OKX_API_PASSPHRASE") or "").strip()
key = normalize_api_credential(os.getenv("OKX_API_KEY"))
secret = normalize_api_credential(os.getenv("OKX_API_SECRET"))
password = normalize_api_credential(os.getenv("OKX_API_PASSPHRASE"))
if not key or not secret or not password:
raise RuntimeError("缺少 OKX_API_KEY / OKX_API_SECRET / OKX_API_PASSPHRASE")
raise RuntimeError("缺少 OKX_API_KEY / OKX_API_SECRET / OKX_API_PASSPHRASE(请在服务器 .env 配置真密钥)")
ex = ccxt.okx(
{
"apiKey": key,
@@ -154,10 +179,10 @@ def _make_exchange() -> Any:
return ex
# gate
key = (os.getenv("GATE_API_KEY") or "").strip()
secret = (os.getenv("GATE_API_SECRET") or "").strip()
key = normalize_api_credential(os.getenv("GATE_API_KEY"))
secret = normalize_api_credential(os.getenv("GATE_API_SECRET"))
if not key or not secret:
raise RuntimeError("缺少 GATE_API_KEY / GATE_API_SECRET")
raise RuntimeError("缺少 GATE_API_KEY / GATE_API_SECRET(请在服务器 .env 配置真密钥)")
from lib.exchange.gate_ccxt_lib import gate_ccxt_class
ex = gate_ccxt_class()(
@@ -177,6 +202,7 @@ def _make_exchange() -> Any:
def get_exchange() -> Any:
global _ccxt_ex
_raise_if_auth_cooling()
if _ccxt_ex is None:
_ccxt_ex = _make_exchange()
return _ccxt_ex
@@ -184,9 +210,17 @@ def get_exchange() -> Any:
def _ensure_markets() -> None:
global _markets_loaded
if not _markets_loaded:
if _markets_loaded:
return
_raise_if_auth_cooling()
try:
get_exchange().load_markets()
_markets_loaded = True
except Exception as e:
if is_exchange_auth_error(e):
_mark_auth_failure(e)
raise RuntimeError(_AUTH_FAIL_MSG) from e
raise
def _check_token(x_control_token: str | None) -> None:
@@ -572,8 +606,20 @@ def _status_inner(x_control_token: str | None) -> Any:
u = bal.get("USDT") or {}
if isinstance(u, dict) and u.get("total") is not None:
balance_usdt = _finite_or_none(u["total"])
except Exception:
pass
except Exception as e:
if is_exchange_auth_error(e):
_mark_auth_failure(e)
return JSONResponse(
{
"ok": False,
"error": _AUTH_FAIL_MSG,
"exchange": EXCHANGE_KIND,
"balance_usdt": None,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
positions_out: list[dict[str, Any]] = []
total_upnl = 0.0
@@ -587,6 +633,19 @@ def _status_inner(x_control_token: str | None) -> Any:
else:
raw = ex.fetch_positions() or []
except Exception as e:
if is_exchange_auth_error(e):
_mark_auth_failure(e)
return JSONResponse(
{
"ok": False,
"error": _AUTH_FAIL_MSG,
"exchange": EXCHANGE_KIND,
"balance_usdt": balance_usdt,
"positions": [],
"total_unrealized_pnl": None,
},
status_code=200,
)
return JSONResponse(
{
"ok": False,