交易所 API 改为仅服务器配置:前端去掉密钥、新机示例为空,并防止坏钥反复请求触发 Gate 封 IP。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+72
-13
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user