交易所 API 改为仅服务器配置:前端去掉密钥、新机示例为空,并防止坏钥反复请求触发 Gate 封 IP。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -76,10 +76,9 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
|||||||
|
|
||||||
# 是否开启 Binance 实盘下单(false=只做本地流程,true=真实下单)
|
# 是否开启 Binance 实盘下单(false=只做本地流程,true=真实下单)
|
||||||
LIVE_TRADING_ENABLED=true
|
LIVE_TRADING_ENABLED=true
|
||||||
# Binance API Key(需开通合约,万向划转等权限)
|
# Binance API(仅服务器 .env 配置;新机保持为空,填真钥后 pm2 restart --update-env;勿用占位符以免鉴权狂打)
|
||||||
BINANCE_API_KEY=REPLACE_WITH_BINANCE_API_KEY
|
BINANCE_API_KEY=
|
||||||
# Binance API Secret
|
BINANCE_API_SECRET=
|
||||||
BINANCE_API_SECRET=REPLACE_WITH_BINANCE_API_SECRET
|
|
||||||
# 保证金模式:cross=全仓,isolated=逐仓
|
# 保证金模式:cross=全仓,isolated=逐仓
|
||||||
BINANCE_MARGIN_MODE=cross
|
BINANCE_MARGIN_MODE=cross
|
||||||
# 持仓模式:hedge=双向(需账户开启双向持仓,下单带 positionSide);oneway=单向
|
# 持仓模式:hedge=双向(需账户开启双向持仓,下单带 positionSide);oneway=单向
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ import sys
|
|||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from lib.paths import common_static_dir
|
from lib.paths import common_static_dir
|
||||||
|
from lib.exchange.api_credentials_lib import (
|
||||||
|
is_exchange_auth_error,
|
||||||
|
load_markets_public_fallback,
|
||||||
|
normalize_api_credential,
|
||||||
|
)
|
||||||
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from lib.ai.ai_review_lib import (
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
@@ -347,8 +352,8 @@ def _resolve_app_tz():
|
|||||||
|
|
||||||
APP_TZ = _resolve_app_tz()
|
APP_TZ = _resolve_app_tz()
|
||||||
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
||||||
BINANCE_API_KEY = (os.getenv("BINANCE_API_KEY") or "").strip()
|
BINANCE_API_KEY = normalize_api_credential(os.getenv("BINANCE_API_KEY"))
|
||||||
BINANCE_API_SECRET = (os.getenv("BINANCE_API_SECRET") or "").strip()
|
BINANCE_API_SECRET = normalize_api_credential(os.getenv("BINANCE_API_SECRET"))
|
||||||
BINANCE_MARGIN_MODE = (os.getenv("BINANCE_MARGIN_MODE") or "cross").strip().lower()
|
BINANCE_MARGIN_MODE = (os.getenv("BINANCE_MARGIN_MODE") or "cross").strip().lower()
|
||||||
# hedge=双向持仓(需 positionSide);oneway / single=单向持仓
|
# hedge=双向持仓(需 positionSide);oneway / single=单向持仓
|
||||||
_raw_binance_pos = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
|
_raw_binance_pos = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
|
||||||
@@ -490,6 +495,8 @@ if BINANCE_API_KEY and BINANCE_API_SECRET:
|
|||||||
exchange.apiKey = BINANCE_API_KEY
|
exchange.apiKey = BINANCE_API_KEY
|
||||||
exchange.secret = BINANCE_API_SECRET
|
exchange.secret = BINANCE_API_SECRET
|
||||||
MARKETS_LOADED = False
|
MARKETS_LOADED = False
|
||||||
|
# 鉴权失败后停止私有 API(资金/持仓),避免坏钥反复请求;尤其 Gate 易封 IP
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||||
ACCOUNT_BALANCE_CACHE = {
|
ACCOUNT_BALANCE_CACHE = {
|
||||||
"updated_at": 0.0,
|
"updated_at": 0.0,
|
||||||
"funding_usdt": None,
|
"funding_usdt": None,
|
||||||
@@ -2859,6 +2866,8 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
|
|
||||||
|
|
||||||
def ensure_exchange_live_ready():
|
def ensure_exchange_live_ready():
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||||
if not LIVE_TRADING_ENABLED:
|
if not LIVE_TRADING_ENABLED:
|
||||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||||
if not (BINANCE_API_KEY and BINANCE_API_SECRET):
|
if not (BINANCE_API_KEY and BINANCE_API_SECRET):
|
||||||
@@ -2893,9 +2902,23 @@ def order_row_key_signal_type(row):
|
|||||||
|
|
||||||
def exchange_private_api_configured():
|
def exchange_private_api_configured():
|
||||||
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False
|
||||||
return bool(BINANCE_API_KEY and BINANCE_API_SECRET)
|
return bool(BINANCE_API_KEY and BINANCE_API_SECRET)
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_private_api_after_auth_error(exc):
|
||||||
|
global EXCHANGE_AUTH_DISABLED_MSG, BINANCE_API_KEY, BINANCE_API_SECRET
|
||||||
|
from lib.exchange.api_credentials_lib import strip_ccxt_credentials
|
||||||
|
|
||||||
|
strip_ccxt_credentials(exchange)
|
||||||
|
BINANCE_API_KEY = ""
|
||||||
|
BINANCE_API_SECRET = ""
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = (
|
||||||
|
f"API 鉴权失败,已停止私有请求(请在服务器 .env 修正密钥后重启): {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _float_balance_field(val):
|
def _float_balance_field(val):
|
||||||
if val is None or val == "":
|
if val is None or val == "":
|
||||||
return None
|
return None
|
||||||
@@ -3178,11 +3201,15 @@ def get_exchange_capitals(force=False):
|
|||||||
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
||||||
try:
|
try:
|
||||||
ACCOUNT_BALANCE_CACHE["funding_usdt"] = _fetch_binance_funding_usdt()
|
ACCOUNT_BALANCE_CACHE["funding_usdt"] = _fetch_binance_funding_usdt()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
if is_exchange_auth_error(e):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
ACCOUNT_BALANCE_CACHE["funding_usdt"] = None
|
ACCOUNT_BALANCE_CACHE["funding_usdt"] = None
|
||||||
try:
|
try:
|
||||||
ACCOUNT_BALANCE_CACHE["trading_usdt"] = _fetch_binance_swap_usdt_total()
|
ACCOUNT_BALANCE_CACHE["trading_usdt"] = _fetch_binance_swap_usdt_total()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
if is_exchange_auth_error(e):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
# 勿保留上一次成功请求的旧值:鉴权失败时否则会误以为「合约余额仍能读」
|
# 勿保留上一次成功请求的旧值:鉴权失败时否则会误以为「合约余额仍能读」
|
||||||
ACCOUNT_BALANCE_CACHE["trading_usdt"] = None
|
ACCOUNT_BALANCE_CACHE["trading_usdt"] = None
|
||||||
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
||||||
@@ -3585,7 +3612,15 @@ def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
|||||||
def ensure_markets_loaded(force=False):
|
def ensure_markets_loaded(force=False):
|
||||||
global MARKETS_LOADED
|
global MARKETS_LOADED
|
||||||
if force or not MARKETS_LOADED:
|
if force or not MARKETS_LOADED:
|
||||||
exchange.load_markets(reload=force)
|
try:
|
||||||
|
exchange.load_markets(reload=force)
|
||||||
|
except Exception as e:
|
||||||
|
# 坏钥时立刻去掉签名再拉公开 markets,避免反复鉴权(尤其勿拖累同机 Gate)
|
||||||
|
if is_exchange_auth_error(e) and (getattr(exchange, "apiKey", None) or getattr(exchange, "secret", None)):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
|
load_markets_public_fallback(exchange, reload=True)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
MARKETS_LOADED = True
|
MARKETS_LOADED = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,10 +74,9 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
|||||||
|
|
||||||
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
|
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
|
||||||
LIVE_TRADING_ENABLED=true
|
LIVE_TRADING_ENABLED=true
|
||||||
# Gate API Key(实盘)
|
# Gate API(仅服务器 .env 配置;新机保持为空,填真钥后重启;错误密钥反复请求易导致 Gate 封 IP)
|
||||||
GATE_API_KEY=REPLACE_WITH_GATE_API_KEY
|
GATE_API_KEY=
|
||||||
# Gate API Secret(实盘)
|
GATE_API_SECRET=
|
||||||
GATE_API_SECRET=REPLACE_WITH_GATE_API_SECRET
|
|
||||||
# 保证金模式:cross=全仓,isolated=逐仓
|
# 保证金模式:cross=全仓,isolated=逐仓
|
||||||
GATE_TD_MODE=cross
|
GATE_TD_MODE=cross
|
||||||
# 持仓筛选:hedge=双向持仓下按多空腿过滤;其它值(如 single)不按腿过滤
|
# 持仓筛选:hedge=双向持仓下按多空腿过滤;其它值(如 single)不按腿过滤
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ import sys
|
|||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from lib.paths import common_static_dir
|
from lib.paths import common_static_dir
|
||||||
|
from lib.exchange.api_credentials_lib import (
|
||||||
|
is_exchange_auth_error,
|
||||||
|
load_markets_public_fallback,
|
||||||
|
normalize_api_credential,
|
||||||
|
)
|
||||||
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from lib.ai.ai_review_lib import (
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
@@ -346,8 +351,8 @@ def _resolve_app_tz():
|
|||||||
|
|
||||||
APP_TZ = _resolve_app_tz()
|
APP_TZ = _resolve_app_tz()
|
||||||
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
||||||
GATE_API_KEY = (os.getenv("GATE_API_KEY") or "").strip()
|
GATE_API_KEY = normalize_api_credential(os.getenv("GATE_API_KEY"))
|
||||||
GATE_API_SECRET = (os.getenv("GATE_API_SECRET") or "").strip()
|
GATE_API_SECRET = normalize_api_credential(os.getenv("GATE_API_SECRET"))
|
||||||
GATE_TD_MODE = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
|
GATE_TD_MODE = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
|
||||||
GATE_POS_MODE = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
|
GATE_POS_MODE = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
|
||||||
# 永续仓位止盈止损触发单:POST /futures/{settle}/price_orders,order_type=close-*-position(全平)
|
# 永续仓位止盈止损触发单:POST /futures/{settle}/price_orders,order_type=close-*-position(全平)
|
||||||
@@ -478,6 +483,8 @@ if GATE_API_KEY and GATE_API_SECRET:
|
|||||||
exchange.apiKey = GATE_API_KEY
|
exchange.apiKey = GATE_API_KEY
|
||||||
exchange.secret = GATE_API_SECRET
|
exchange.secret = GATE_API_SECRET
|
||||||
MARKETS_LOADED = False
|
MARKETS_LOADED = False
|
||||||
|
# 鉴权失败后停止私有 API,避免坏钥反复签名;Gate 尤其易封 IP
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||||
ACCOUNT_BALANCE_CACHE = {
|
ACCOUNT_BALANCE_CACHE = {
|
||||||
"updated_at": 0.0,
|
"updated_at": 0.0,
|
||||||
"funding_usdt": None,
|
"funding_usdt": None,
|
||||||
@@ -2547,6 +2554,8 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
|
|
||||||
|
|
||||||
def ensure_exchange_live_ready():
|
def ensure_exchange_live_ready():
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||||
if not LIVE_TRADING_ENABLED:
|
if not LIVE_TRADING_ENABLED:
|
||||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||||
if not (GATE_API_KEY and GATE_API_SECRET):
|
if not (GATE_API_KEY and GATE_API_SECRET):
|
||||||
@@ -2581,9 +2590,23 @@ def order_row_key_signal_type(row):
|
|||||||
|
|
||||||
def exchange_private_api_configured():
|
def exchange_private_api_configured():
|
||||||
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False
|
||||||
return bool(GATE_API_KEY and GATE_API_SECRET)
|
return bool(GATE_API_KEY and GATE_API_SECRET)
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_private_api_after_auth_error(exc):
|
||||||
|
global EXCHANGE_AUTH_DISABLED_MSG, GATE_API_KEY, GATE_API_SECRET
|
||||||
|
from lib.exchange.api_credentials_lib import strip_ccxt_credentials
|
||||||
|
|
||||||
|
strip_ccxt_credentials(exchange)
|
||||||
|
GATE_API_KEY = ""
|
||||||
|
GATE_API_SECRET = ""
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = (
|
||||||
|
f"API 鉴权失败,已停止私有请求(请在服务器 .env 修正密钥后重启;勿反复试错以免 Gate 封 IP): {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _extract_usdt_total(balance):
|
def _extract_usdt_total(balance):
|
||||||
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
||||||
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
||||||
@@ -2841,11 +2864,15 @@ def get_exchange_capitals(force=False):
|
|||||||
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
||||||
try:
|
try:
|
||||||
ACCOUNT_BALANCE_CACHE["funding_usdt"] = _fetch_gate_funding_usdt()
|
ACCOUNT_BALANCE_CACHE["funding_usdt"] = _fetch_gate_funding_usdt()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
if is_exchange_auth_error(e):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
ACCOUNT_BALANCE_CACHE["funding_usdt"] = None
|
ACCOUNT_BALANCE_CACHE["funding_usdt"] = None
|
||||||
try:
|
try:
|
||||||
ACCOUNT_BALANCE_CACHE["trading_usdt"] = _fetch_usdt_by_types(["swap", "spot"])
|
ACCOUNT_BALANCE_CACHE["trading_usdt"] = _fetch_usdt_by_types(["swap", "spot"])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
if is_exchange_auth_error(e):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
# 勿保留上一次成功请求的旧值:鉴权失败时否则会误以为「合约余额仍能读」
|
# 勿保留上一次成功请求的旧值:鉴权失败时否则会误以为「合约余额仍能读」
|
||||||
ACCOUNT_BALANCE_CACHE["trading_usdt"] = None
|
ACCOUNT_BALANCE_CACHE["trading_usdt"] = None
|
||||||
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
||||||
@@ -3345,7 +3372,15 @@ def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
|||||||
def ensure_markets_loaded(force=False):
|
def ensure_markets_loaded(force=False):
|
||||||
global MARKETS_LOADED
|
global MARKETS_LOADED
|
||||||
if force or not MARKETS_LOADED:
|
if force or not MARKETS_LOADED:
|
||||||
exchange.load_markets(reload=force)
|
try:
|
||||||
|
exchange.load_markets(reload=force)
|
||||||
|
except Exception as e:
|
||||||
|
# Gate 对无效签名/坏钥敏感,失败后立即改公开 markets,勿反复带钥请求
|
||||||
|
if is_exchange_auth_error(e) and (getattr(exchange, "apiKey", None) or getattr(exchange, "secret", None)):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
|
load_markets_public_fallback(exchange, reload=True)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
MARKETS_LOADED = True
|
MARKETS_LOADED = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
|||||||
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
|
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
|
||||||
LIVE_TRADING_ENABLED=true
|
LIVE_TRADING_ENABLED=true
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# OKX 账户 API(永续 + 期权共用同一套密钥;修改后须重启 PM2)
|
# OKX 账户 API(永续+期权共用;仅服务器 .env 手改,前端不展示)
|
||||||
|
# 新机保持为空;填真钥后 pm2 restart --update-env(含子代理)
|
||||||
# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填
|
# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
|
OKX_API_KEY=
|
||||||
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
|
OKX_API_SECRET=
|
||||||
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
|
OKX_API_PASSPHRASE=
|
||||||
# 保证金模式:cross=全仓,isolated=逐仓
|
# 保证金模式:cross=全仓,isolated=逐仓
|
||||||
OKX_TD_MODE=cross
|
OKX_TD_MODE=cross
|
||||||
# 持仓模式:hedge=双向持仓,net=单向净持仓
|
# 持仓模式:hedge=双向持仓,net=单向净持仓
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ import sys
|
|||||||
if _REPO_ROOT not in sys.path:
|
if _REPO_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from lib.paths import common_static_dir
|
from lib.paths import common_static_dir
|
||||||
|
from lib.exchange.api_credentials_lib import (
|
||||||
|
is_exchange_auth_error,
|
||||||
|
load_markets_public_fallback,
|
||||||
|
normalize_api_credential,
|
||||||
|
)
|
||||||
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
from lib.ai.ai_client import ai_generate, ai_review, ai_short_advice
|
||||||
from lib.ai.ai_review_lib import (
|
from lib.ai.ai_review_lib import (
|
||||||
build_journal_ai_chart_path,
|
build_journal_ai_chart_path,
|
||||||
@@ -360,9 +365,9 @@ def _promote_legacy_options_api_keys() -> None:
|
|||||||
|
|
||||||
|
|
||||||
_promote_legacy_options_api_keys()
|
_promote_legacy_options_api_keys()
|
||||||
OKX_API_KEY = os.getenv("OKX_API_KEY", "")
|
OKX_API_KEY = normalize_api_credential(os.getenv("OKX_API_KEY"))
|
||||||
OKX_API_SECRET = os.getenv("OKX_API_SECRET", "")
|
OKX_API_SECRET = normalize_api_credential(os.getenv("OKX_API_SECRET"))
|
||||||
OKX_API_PASSPHRASE = os.getenv("OKX_API_PASSPHRASE", "")
|
OKX_API_PASSPHRASE = normalize_api_credential(os.getenv("OKX_API_PASSPHRASE"))
|
||||||
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "false").lower() in ("1", "true", "yes", "on")
|
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "false").lower() in ("1", "true", "yes", "on")
|
||||||
OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10"))
|
OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10"))
|
||||||
OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
|
OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
|
||||||
@@ -506,6 +511,7 @@ if OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE:
|
|||||||
exchange_options.password = OKX_API_PASSPHRASE
|
exchange_options.password = OKX_API_PASSPHRASE
|
||||||
|
|
||||||
MARKETS_LOADED = False
|
MARKETS_LOADED = False
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||||
ACCOUNT_BALANCE_CACHE = {
|
ACCOUNT_BALANCE_CACHE = {
|
||||||
"updated_at": 0.0,
|
"updated_at": 0.0,
|
||||||
"funding_usdt": None,
|
"funding_usdt": None,
|
||||||
@@ -2485,6 +2491,8 @@ def enrich_order_item(raw_item, current_capital):
|
|||||||
|
|
||||||
|
|
||||||
def ensure_okx_live_ready():
|
def ensure_okx_live_ready():
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||||
if not LIVE_TRADING_ENABLED:
|
if not LIVE_TRADING_ENABLED:
|
||||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||||
if not (OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE):
|
if not (OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE):
|
||||||
@@ -2517,6 +2525,23 @@ def order_row_key_signal_type(row):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_private_api_after_auth_error(exc):
|
||||||
|
global EXCHANGE_AUTH_DISABLED_MSG, OKX_API_KEY, OKX_API_SECRET, OKX_API_PASSPHRASE
|
||||||
|
from lib.exchange.api_credentials_lib import strip_ccxt_credentials
|
||||||
|
|
||||||
|
strip_ccxt_credentials(exchange)
|
||||||
|
try:
|
||||||
|
strip_ccxt_credentials(exchange_options)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
OKX_API_KEY = ""
|
||||||
|
OKX_API_SECRET = ""
|
||||||
|
OKX_API_PASSPHRASE = ""
|
||||||
|
EXCHANGE_AUTH_DISABLED_MSG = (
|
||||||
|
f"API 鉴权失败,已停止私有请求(请在服务器 .env 修正密钥后重启): {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _extract_usdt_total(balance):
|
def _extract_usdt_total(balance):
|
||||||
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
||||||
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
||||||
@@ -2637,8 +2662,9 @@ def get_exchange_capitals(force=False):
|
|||||||
ACCOUNT_BALANCE_CACHE["funding_usdt"] = funding
|
ACCOUNT_BALANCE_CACHE["funding_usdt"] = funding
|
||||||
ACCOUNT_BALANCE_CACHE["trading_usdt"] = trading
|
ACCOUNT_BALANCE_CACHE["trading_usdt"] = trading
|
||||||
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
if is_exchange_auth_error(e):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
||||||
|
|
||||||
|
|
||||||
@@ -2869,7 +2895,14 @@ def build_okx_order_params(direction, reduce_only=False):
|
|||||||
def ensure_markets_loaded(force=False):
|
def ensure_markets_loaded(force=False):
|
||||||
global MARKETS_LOADED
|
global MARKETS_LOADED
|
||||||
if force or not MARKETS_LOADED:
|
if force or not MARKETS_LOADED:
|
||||||
exchange.load_markets(reload=force)
|
try:
|
||||||
|
exchange.load_markets(reload=force)
|
||||||
|
except Exception as e:
|
||||||
|
if is_exchange_auth_error(e) and (getattr(exchange, "apiKey", None) or getattr(exchange, "secret", None)):
|
||||||
|
_disable_private_api_after_auth_error(e)
|
||||||
|
load_markets_public_fallback(exchange, reload=True)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
MARKETS_LOADED = True
|
MARKETS_LOADED = True
|
||||||
|
|
||||||
|
|
||||||
@@ -2985,6 +3018,8 @@ def _okx_place_tp_sl_orders(exchange_symbol, direction, amount, stop_loss, take_
|
|||||||
|
|
||||||
|
|
||||||
def exchange_private_api_configured():
|
def exchange_private_api_configured():
|
||||||
|
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||||
|
return False
|
||||||
return bool(OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE)
|
return bool(OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -40,9 +40,9 @@ bash /opt/crypto_monitor/deploy/manage.sh
|
|||||||
|
|
||||||
- 登录账号: **admin**
|
- 登录账号: **admin**
|
||||||
- 登录密码: **admin123**
|
- 登录密码: **admin123**
|
||||||
- 浏览器配置: 各所 **env 配置**(API,风控) + 中控 **系统设置**
|
- 浏览器配置: 各所 **env 配置**(风控等) + 中控 **系统设置**;**交易所 API 仅服务器 `.env` 手改**(新机默认为空)
|
||||||
|
|
||||||
**无需 SSH 编辑 `.env` 填 API**;密钥由 `bootstrap_deploy_secrets.py` 自动生成.
|
**无需 SSH 编辑 `.env` 填通信/登录类密钥**;交易所 API 须在服务器写入各所 `.env`(新机为空).
|
||||||
|
|
||||||
| 地址 | 端口 |
|
| 地址 | 端口 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
|||||||
+5
-4
@@ -14,6 +14,7 @@
|
|||||||
| **前端仅中文** | 页面只显示中文标签与说明,不显示 `APP_XXX` 等变量名 |
|
| **前端仅中文** | 页面只显示中文标签与说明,不显示 `APP_XXX` 等变量名 |
|
||||||
| **账户密码不进本页** | 登录用户名/密码在 **系统设置 → 账户密码修改** 中维护 |
|
| **账户密码不进本页** | 登录用户名/密码在 **系统设置 → 账户密码修改** 中维护 |
|
||||||
| **密钥自动托管** | 中控通信密钥,登录会话密钥由 **首次部署脚本自动生成并写入**(一次生成,不轮换),本页不提供编辑 |
|
| **密钥自动托管** | 中控通信密钥,登录会话密钥由 **首次部署脚本自动生成并写入**(一次生成,不轮换),本页不提供编辑 |
|
||||||
|
| **交易所 API 不进本页** | `OKX/BINANCE/GATE_API_*` 仅在服务器实例目录 `.env` 配置;新机默认为空,填真钥后 `pm2 restart --update-env` |
|
||||||
| **AI 仅中控配置** | OpenAI / Ollama 等 AI 项已从中控 **系统设置 → AI 配置** 统一维护并同步三所,本页不再展示 |
|
| **AI 仅中控配置** | OpenAI / Ollama 等 AI 项已从中控 **系统设置 → AI 配置** 统一维护并同步三所,本页不再展示 |
|
||||||
| **保存标注** | 每项标注「保存即生效」或「需重启」;含需重启项时可用「保存并重启」 |
|
| **保存标注** | 每项标注「保存即生效」或「需重启」;含需重启项时可用「保存并重启」 |
|
||||||
|
|
||||||
@@ -61,15 +62,14 @@ Binance / Gate 无期权模块时,第三列最后一格不显示或显示「本
|
|||||||
| 中文名 | 说明 | 重启 |
|
| 中文名 | 说明 | 重启 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| 开启实盘下单 | 关闭时仅走本地流程,不向交易所发单 | 需重启 |
|
| 开启实盘下单 | 关闭时仅走本地流程,不向交易所发单 | 需重启 |
|
||||||
| API Key | 账户 API Key(永续+期权共用) | 需重启 |
|
|
||||||
| API Secret | 账户 API Secret | 需重启 |
|
|
||||||
| API Passphrase | 仅 OKX 显示 | 需重启 |
|
|
||||||
| 保证金模式 | 全仓 / 逐仓 | 需重启 |
|
| 保证金模式 | 全仓 / 逐仓 | 需重启 |
|
||||||
| 持仓模式 | 双向 / 单向净持仓等(按所) | 需重启 |
|
| 持仓模式 | 双向 / 单向净持仓等(按所) | 需重启 |
|
||||||
| 仓位查询类型 | 仅 OKX:如 SWAP | 需重启 |
|
| 仓位查询类型 | 仅 OKX:如 SWAP | 需重启 |
|
||||||
| 账户备注 | 企业微信推送中显示的交易所备注 | 保存即生效 |
|
| 账户备注 | 企业微信推送中显示的交易所备注 | 保存即生效 |
|
||||||
| 显示永续资金 | 仅 OKX:关闭后顶栏隐藏 USDT 资金/交易账户,总资金仅计期权 USDC 侧 | 保存即生效 |
|
| 显示永续资金 | 仅 OKX:关闭后顶栏隐藏 USDT 资金/交易账户,总资金仅计期权 USDC 侧 | 保存即生效 |
|
||||||
|
|
||||||
|
**交易所 API Key / Secret / Passphrase 不在本页**:请 SSH 编辑各所 `crypto_monitor_*/.env`,新机部署后应为空;配好真钥后重启对应 Flask 与子代理(`pm2 restart … --update-env`).占位符或错误密钥会导致鉴权失败,Gate 上反复请求还可能封 IP.
|
||||||
|
|
||||||
**本卡片不包含**:网页登录账号密码,是否关闭登录校验,中控通信密钥.
|
**本卡片不包含**:网页登录账号密码,是否关闭登录校验,中控通信密钥.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -196,6 +196,7 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT
|
|||||||
|
|
||||||
- 服务:`APP_HOST`,`APP_PORT`,`APP_DEBUG`
|
- 服务:`APP_HOST`,`APP_PORT`,`APP_DEBUG`
|
||||||
- 数据:`DB_PATH`,`UPLOAD_DIR`
|
- 数据:`DB_PATH`,`UPLOAD_DIR`
|
||||||
|
- **交易所 API**:`OKX_API_*`,`BINANCE_API_*`,`GATE_API_*`(仅 SSH;新机应为空)
|
||||||
- 关键位门控:全部 `KEY_*`,`KLINE_*`
|
- 关键位门控:全部 `KEY_*`,`KLINE_*`
|
||||||
- 轮询与同步:`BALANCE_REFRESH_SECONDS`,`PRICE_REFRESH_SECONDS`,`MONITOR_POLL_SECONDS`,`BREAKEVEN_*`,`RECONCILE_*`
|
- 轮询与同步:`BALANCE_REFRESH_SECONDS`,`PRICE_REFRESH_SECONDS`,`MONITOR_POLL_SECONDS`,`BREAKEVEN_*`,`RECONCILE_*`
|
||||||
- 代理:`OKX_SOCKS_PROXY`,`BINANCE_HTTP_PROXY` 等
|
- 代理:`OKX_SOCKS_PROXY`,`BINANCE_HTTP_PROXY` 等
|
||||||
@@ -212,7 +213,7 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT
|
|||||||
| 能力 | env 配置 | 系统设置 | 中控系统设置 |
|
| 能力 | env 配置 | 系统设置 | 中控系统设置 |
|
||||||
|------|----------|----------|--------------|
|
|------|----------|----------|--------------|
|
||||||
| 登录用户名/密码 | ❌ | ✅ 账户密码修改 | ✅ 中控账户密码 |
|
| 登录用户名/密码 | ❌ | ✅ 账户密码修改 | ✅ 中控账户密码 |
|
||||||
| 交易所 API | ✅(各所自配) | ❌ | ❌ |
|
| 交易所 API | ❌(仅服务器 `.env`) | ❌ | ❌ |
|
||||||
| AI / OpenAI | ❌ | ❌ | ✅ AI 配置(同步三所) |
|
| AI / OpenAI | ❌ | ❌ | ✅ AI 配置(同步三所) |
|
||||||
| 导航/区块显示 | ❌ | ✅ 导航显示 | ✅ 显示与导航 |
|
| 导航/区块显示 | ❌ | ✅ 导航显示 | ✅ 显示与导航 |
|
||||||
| 手动资金划转 | ❌ | ✅ 永续资金划转 | ❌ |
|
| 手动资金划转 | ❌ | ✅ 永续资金划转 | ❌ |
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ python3 scripts/bootstrap_deploy_secrets.py
|
|||||||
|
|
||||||
## 4. 实例 env 配置页变更
|
## 4. 实例 env 配置页变更
|
||||||
|
|
||||||
三所 **env 配置** 页已 **移除「AI 复盘」卡片**.交易所 API,企业微信,交易执行等仍各所自配.
|
三所 **env 配置** 页已 **移除「AI 复盘」卡片**.企业微信、交易执行等仍各所自配;**交易所 API 仅服务器 `.env`**,前端不再展示.
|
||||||
|
|
||||||
实例侧若通过 API 提交已移除的 AI 键,会被白名单过滤,不会写入.
|
实例侧若通过 API 提交已移除的 AI 键,会被白名单过滤,不会写入.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@
|
|||||||
| 网页登录密码 | ✅ | 本区块 |
|
| 网页登录密码 | ✅ | 本区块 |
|
||||||
| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成,中控与实例一致 |
|
| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成,中控与实例一致 |
|
||||||
| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成,三所相同 |
|
| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成,三所相同 |
|
||||||
| 交易所 API | ❌ | 在 **env 配置** 页(各所自配) |
|
| 交易所 API | ❌ | **仅服务器** 各所 `.env`(`*_API_KEY` 等;前端 env 页已移除) |
|
||||||
| AI 复盘 / OpenAI | ❌ | 在中控 **系统设置 → AI 配置**(同步三所) |
|
| AI 复盘 / OpenAI | ❌ | 在中控 **系统设置 → AI 配置**(同步三所) |
|
||||||
|
|
||||||
### 操作流程
|
### 操作流程
|
||||||
|
|||||||
Vendored
+3
-10
@@ -19,10 +19,7 @@ from lib.env.env_schema import (
|
|||||||
# 各所「交易所与实盘」字段(顺序即页面顺序)
|
# 各所「交易所与实盘」字段(顺序即页面顺序)
|
||||||
_EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
|
_EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
|
||||||
"okx": [
|
"okx": [
|
||||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||||
("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"),
|
|
||||||
("OKX_API_SECRET", "API Secret", "账户 API(永续+期权共用)"),
|
|
||||||
("OKX_API_PASSPHRASE", "API Passphrase", "OKX 必填"),
|
|
||||||
("OKX_TD_MODE", "保证金模式", ""),
|
("OKX_TD_MODE", "保证金模式", ""),
|
||||||
("OKX_POS_MODE", "持仓模式", ""),
|
("OKX_POS_MODE", "持仓模式", ""),
|
||||||
("OKX_POSITION_INST_TYPE", "仓位查询类型", "如 SWAP"),
|
("OKX_POSITION_INST_TYPE", "仓位查询类型", "如 SWAP"),
|
||||||
@@ -34,17 +31,13 @@ _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
"binance": [
|
"binance": [
|
||||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||||
("BINANCE_API_KEY", "API Key", "永续子账户"),
|
|
||||||
("BINANCE_API_SECRET", "API Secret", "永续子账户"),
|
|
||||||
("BINANCE_MARGIN_MODE", "保证金模式", ""),
|
("BINANCE_MARGIN_MODE", "保证金模式", ""),
|
||||||
("BINANCE_POSITION_MODE", "持仓模式", ""),
|
("BINANCE_POSITION_MODE", "持仓模式", ""),
|
||||||
("BINANCE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
|
("BINANCE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
|
||||||
],
|
],
|
||||||
"gate": [
|
"gate": [
|
||||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||||
("GATE_API_KEY", "API Key", "永续子账户"),
|
|
||||||
("GATE_API_SECRET", "API Secret", "永续子账户"),
|
|
||||||
("GATE_TD_MODE", "保证金模式", ""),
|
("GATE_TD_MODE", "保证金模式", ""),
|
||||||
("GATE_POS_MODE", "持仓模式", ""),
|
("GATE_POS_MODE", "持仓模式", ""),
|
||||||
("GATE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
|
("GATE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""交易所 API 凭证规范化.
|
||||||
|
|
||||||
|
新机 .env 密钥应为空;示例占位符不得注入 ccxt,否则鉴权失败且
|
||||||
|
(尤其 Gate)反复签名请求易触发 IP 封禁.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
_PLACEHOLDER_EXACT = frozenset(
|
||||||
|
{
|
||||||
|
"你的密钥",
|
||||||
|
"your-api-key",
|
||||||
|
"your_api_key",
|
||||||
|
"your-api-secret",
|
||||||
|
"your_api_secret",
|
||||||
|
"todo",
|
||||||
|
"xxx",
|
||||||
|
"changeme",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_api_credential(value: Optional[str]) -> str:
|
||||||
|
"""去空白;空/占位符一律视为未配置."""
|
||||||
|
s = (value or "").strip().strip('"').strip("'")
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
upper = s.upper()
|
||||||
|
if upper.startswith("REPLACE_WITH"):
|
||||||
|
return ""
|
||||||
|
if upper.startswith("CHANGE_TO"):
|
||||||
|
return ""
|
||||||
|
if s.lower() in _PLACEHOLDER_EXACT:
|
||||||
|
return ""
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def credentials_configured(*parts: Optional[str]) -> bool:
|
||||||
|
return all(bool(normalize_api_credential(p)) for p in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def is_exchange_auth_error(exc: BaseException) -> bool:
|
||||||
|
"""鉴权/无效 Key 类错误(用于停掉后续签名请求,避免 Gate 封 IP)."""
|
||||||
|
name = type(exc).__name__
|
||||||
|
if name in ("AuthenticationError", "PermissionDenied", "InvalidNonce"):
|
||||||
|
return True
|
||||||
|
msg = str(exc)
|
||||||
|
markers = (
|
||||||
|
"Invalid Api-Key",
|
||||||
|
"Invalid API-key",
|
||||||
|
"Invalid API Key",
|
||||||
|
"INVALID_KEY",
|
||||||
|
"Invalid key",
|
||||||
|
"API key is invalid",
|
||||||
|
"api key not found",
|
||||||
|
"Signature",
|
||||||
|
"INVALID_SIGNATURE",
|
||||||
|
"401",
|
||||||
|
"-2008",
|
||||||
|
"-2014",
|
||||||
|
"-2015",
|
||||||
|
"10003", # Gate: invalid key often
|
||||||
|
"INVALID_KEY",
|
||||||
|
)
|
||||||
|
low = msg.lower()
|
||||||
|
if "api" in low and ("key" in low or "sign" in low) and (
|
||||||
|
"invalid" in low or "incorrect" in low or "not found" in low
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return any(m in msg for m in markers)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_ccxt_credentials(exchange: Any) -> None:
|
||||||
|
"""内存中清空密钥,后续只走公开接口,避免继续带坏钥签名."""
|
||||||
|
try:
|
||||||
|
exchange.apiKey = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
exchange.secret = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
exchange.password = ""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_markets_public_fallback(exchange: Any, *, reload: bool = False) -> None:
|
||||||
|
"""鉴权失败后去掉密钥再拉公开 markets(最多再请求一次)."""
|
||||||
|
strip_ccxt_credentials(exchange)
|
||||||
|
exchange.load_markets(reload=reload)
|
||||||
+72
-13
@@ -37,6 +37,11 @@ from lib.hub.hub_position_metrics import (
|
|||||||
parse_position_unrealized_pnl,
|
parse_position_unrealized_pnl,
|
||||||
resolve_position_display_upnl,
|
resolve_position_display_upnl,
|
||||||
)
|
)
|
||||||
|
from lib.exchange.api_credentials_lib import (
|
||||||
|
is_exchange_auth_error,
|
||||||
|
normalize_api_credential,
|
||||||
|
strip_ccxt_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
from fastapi import FastAPI, Header, HTTPException, Request
|
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)
|
app = FastAPI(title="sub-agent", docs_url=None, redoc_url=None)
|
||||||
_ccxt_ex: Any = None
|
_ccxt_ex: Any = None
|
||||||
_markets_loaded = False
|
_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:
|
def _socks_proxy_url(prefix: str) -> str:
|
||||||
return (os.getenv(f"{prefix}_SOCKS_PROXY") or "").strip()
|
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:
|
def _http_https_proxy(prefix: str) -> dict[str, str] | None:
|
||||||
http = (os.getenv(f"{prefix}_HTTP_PROXY") or "").strip()
|
http = (os.getenv(f"{prefix}_HTTP_PROXY") or "").strip()
|
||||||
https = (os.getenv(f"{prefix}_HTTPS_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:
|
def _make_exchange() -> Any:
|
||||||
|
_raise_if_auth_cooling()
|
||||||
if EXCHANGE_KIND == "binance":
|
if EXCHANGE_KIND == "binance":
|
||||||
key = (os.getenv("BINANCE_API_KEY") or "").strip()
|
key = normalize_api_credential(os.getenv("BINANCE_API_KEY"))
|
||||||
secret = (os.getenv("BINANCE_API_SECRET") or "").strip()
|
secret = normalize_api_credential(os.getenv("BINANCE_API_SECRET"))
|
||||||
if not key or not 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(
|
ex = ccxt.binance(
|
||||||
{
|
{
|
||||||
"apiKey": key,
|
"apiKey": key,
|
||||||
@@ -133,11 +158,11 @@ def _make_exchange() -> Any:
|
|||||||
return ex
|
return ex
|
||||||
|
|
||||||
if EXCHANGE_KIND == "okx":
|
if EXCHANGE_KIND == "okx":
|
||||||
key = (os.getenv("OKX_API_KEY") or "").strip()
|
key = normalize_api_credential(os.getenv("OKX_API_KEY"))
|
||||||
secret = (os.getenv("OKX_API_SECRET") or "").strip()
|
secret = normalize_api_credential(os.getenv("OKX_API_SECRET"))
|
||||||
password = (os.getenv("OKX_API_PASSPHRASE") or "").strip()
|
password = normalize_api_credential(os.getenv("OKX_API_PASSPHRASE"))
|
||||||
if not key or not secret or not password:
|
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(
|
ex = ccxt.okx(
|
||||||
{
|
{
|
||||||
"apiKey": key,
|
"apiKey": key,
|
||||||
@@ -154,10 +179,10 @@ def _make_exchange() -> Any:
|
|||||||
return ex
|
return ex
|
||||||
|
|
||||||
# gate
|
# gate
|
||||||
key = (os.getenv("GATE_API_KEY") or "").strip()
|
key = normalize_api_credential(os.getenv("GATE_API_KEY"))
|
||||||
secret = (os.getenv("GATE_API_SECRET") or "").strip()
|
secret = normalize_api_credential(os.getenv("GATE_API_SECRET"))
|
||||||
if not key or not 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
|
from lib.exchange.gate_ccxt_lib import gate_ccxt_class
|
||||||
|
|
||||||
ex = gate_ccxt_class()(
|
ex = gate_ccxt_class()(
|
||||||
@@ -177,6 +202,7 @@ def _make_exchange() -> Any:
|
|||||||
|
|
||||||
def get_exchange() -> Any:
|
def get_exchange() -> Any:
|
||||||
global _ccxt_ex
|
global _ccxt_ex
|
||||||
|
_raise_if_auth_cooling()
|
||||||
if _ccxt_ex is None:
|
if _ccxt_ex is None:
|
||||||
_ccxt_ex = _make_exchange()
|
_ccxt_ex = _make_exchange()
|
||||||
return _ccxt_ex
|
return _ccxt_ex
|
||||||
@@ -184,9 +210,17 @@ def get_exchange() -> Any:
|
|||||||
|
|
||||||
def _ensure_markets() -> None:
|
def _ensure_markets() -> None:
|
||||||
global _markets_loaded
|
global _markets_loaded
|
||||||
if not _markets_loaded:
|
if _markets_loaded:
|
||||||
|
return
|
||||||
|
_raise_if_auth_cooling()
|
||||||
|
try:
|
||||||
get_exchange().load_markets()
|
get_exchange().load_markets()
|
||||||
_markets_loaded = True
|
_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:
|
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 {}
|
u = bal.get("USDT") or {}
|
||||||
if isinstance(u, dict) and u.get("total") is not None:
|
if isinstance(u, dict) and u.get("total") is not None:
|
||||||
balance_usdt = _finite_or_none(u["total"])
|
balance_usdt = _finite_or_none(u["total"])
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
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]] = []
|
positions_out: list[dict[str, Any]] = []
|
||||||
total_upnl = 0.0
|
total_upnl = 0.0
|
||||||
@@ -587,6 +633,19 @@ def _status_inner(x_control_token: str | None) -> Any:
|
|||||||
else:
|
else:
|
||||||
raw = ex.fetch_positions() or []
|
raw = ex.fetch_positions() or []
|
||||||
except Exception as e:
|
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(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
"ok": False,
|
"ok": False,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from lib.exchange.api_credentials_lib import (
|
||||||
|
credentials_configured,
|
||||||
|
is_exchange_auth_error,
|
||||||
|
normalize_api_credential,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeApiCredential(unittest.TestCase):
|
||||||
|
def test_empty_and_placeholder(self):
|
||||||
|
self.assertEqual(normalize_api_credential(""), "")
|
||||||
|
self.assertEqual(normalize_api_credential(None), "")
|
||||||
|
self.assertEqual(normalize_api_credential(" "), "")
|
||||||
|
self.assertEqual(normalize_api_credential("REPLACE_WITH_GATE_API_KEY"), "")
|
||||||
|
self.assertEqual(normalize_api_credential("CHANGE_TO_LONG_RANDOM_SECRET"), "")
|
||||||
|
self.assertEqual(normalize_api_credential("你的密钥"), "")
|
||||||
|
|
||||||
|
def test_real_key_kept(self):
|
||||||
|
self.assertEqual(normalize_api_credential(" real-key-value "), "real-key-value")
|
||||||
|
self.assertTrue(credentials_configured("abc", "def"))
|
||||||
|
self.assertFalse(credentials_configured("REPLACE_WITH_X", "secret"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthErrorDetect(unittest.TestCase):
|
||||||
|
def test_binance_invalid_key(self):
|
||||||
|
class AuthenticationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertTrue(is_exchange_auth_error(AuthenticationError('binance {"code":-2008,"msg":"Invalid Api-Key ID."}')))
|
||||||
|
self.assertTrue(is_exchange_auth_error(Exception("gate INVALID_KEY")))
|
||||||
|
self.assertFalse(is_exchange_auth_error(Exception("rate limit exceeded")))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -125,7 +125,10 @@ class TestEnvSchema(unittest.TestCase):
|
|||||||
self.assertTrue(by_key["OKX_SHOW_PERP_FUNDS"].get("hot_reload"))
|
self.assertTrue(by_key["OKX_SHOW_PERP_FUNDS"].get("hot_reload"))
|
||||||
self.assertNotIn("OKX_OPTIONS_API_KEY", by_key)
|
self.assertNotIn("OKX_OPTIONS_API_KEY", by_key)
|
||||||
self.assertNotIn("OKX_SUB_ACCOUNT_NAME", by_key)
|
self.assertNotIn("OKX_SUB_ACCOUNT_NAME", by_key)
|
||||||
self.assertEqual(by_key["OKX_API_KEY"]["note"], "账户 API(永续+期权共用)")
|
self.assertNotIn("OKX_API_KEY", by_key)
|
||||||
|
self.assertNotIn("OKX_API_SECRET", by_key)
|
||||||
|
self.assertNotIn("OKX_API_PASSPHRASE", by_key)
|
||||||
|
self.assertIn("LIVE_TRADING_ENABLED", by_key)
|
||||||
|
|
||||||
groups_v = [{"title": "t", "fields": [by_key["TRADE_DIRECTION"]]}]
|
groups_v = [{"title": "t", "fields": [by_key["TRADE_DIRECTION"]]}]
|
||||||
clean, errors = validate_env_updates(groups_v, {"TRADE_DIRECTION": "long_only"})
|
clean, errors = validate_env_updates(groups_v, {"TRADE_DIRECTION": "long_only"})
|
||||||
|
|||||||
Reference in New Issue
Block a user