交易所 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=真实下单)
|
||||
LIVE_TRADING_ENABLED=true
|
||||
# Binance API Key(需开通合约,万向划转等权限)
|
||||
BINANCE_API_KEY=REPLACE_WITH_BINANCE_API_KEY
|
||||
# Binance API Secret
|
||||
BINANCE_API_SECRET=REPLACE_WITH_BINANCE_API_SECRET
|
||||
# Binance API(仅服务器 .env 配置;新机保持为空,填真钥后 pm2 restart --update-env;勿用占位符以免鉴权狂打)
|
||||
BINANCE_API_KEY=
|
||||
BINANCE_API_SECRET=
|
||||
# 保证金模式:cross=全仓,isolated=逐仓
|
||||
BINANCE_MARGIN_MODE=cross
|
||||
# 持仓模式:hedge=双向(需账户开启双向持仓,下单带 positionSide);oneway=单向
|
||||
|
||||
@@ -35,6 +35,11 @@ import sys
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
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_review_lib import (
|
||||
build_journal_ai_chart_path,
|
||||
@@ -347,8 +352,8 @@ def _resolve_app_tz():
|
||||
|
||||
APP_TZ = _resolve_app_tz()
|
||||
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
||||
BINANCE_API_KEY = (os.getenv("BINANCE_API_KEY") or "").strip()
|
||||
BINANCE_API_SECRET = (os.getenv("BINANCE_API_SECRET") or "").strip()
|
||||
BINANCE_API_KEY = normalize_api_credential(os.getenv("BINANCE_API_KEY"))
|
||||
BINANCE_API_SECRET = normalize_api_credential(os.getenv("BINANCE_API_SECRET"))
|
||||
BINANCE_MARGIN_MODE = (os.getenv("BINANCE_MARGIN_MODE") or "cross").strip().lower()
|
||||
# hedge=双向持仓(需 positionSide);oneway / single=单向持仓
|
||||
_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.secret = BINANCE_API_SECRET
|
||||
MARKETS_LOADED = False
|
||||
# 鉴权失败后停止私有 API(资金/持仓),避免坏钥反复请求;尤其 Gate 易封 IP
|
||||
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||
ACCOUNT_BALANCE_CACHE = {
|
||||
"updated_at": 0.0,
|
||||
"funding_usdt": None,
|
||||
@@ -2859,6 +2866,8 @@ def enrich_order_item(raw_item, current_capital):
|
||||
|
||||
|
||||
def ensure_exchange_live_ready():
|
||||
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||
if not LIVE_TRADING_ENABLED:
|
||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||
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():
|
||||
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
||||
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||
return False
|
||||
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):
|
||||
if val is None or val == "":
|
||||
return None
|
||||
@@ -3178,11 +3201,15 @@ def get_exchange_capitals(force=False):
|
||||
return ACCOUNT_BALANCE_CACHE["funding_usdt"], ACCOUNT_BALANCE_CACHE["trading_usdt"]
|
||||
try:
|
||||
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
|
||||
try:
|
||||
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["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):
|
||||
global 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
|
||||
|
||||
|
||||
|
||||
@@ -74,10 +74,9 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
||||
|
||||
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
|
||||
LIVE_TRADING_ENABLED=true
|
||||
# Gate API Key(实盘)
|
||||
GATE_API_KEY=REPLACE_WITH_GATE_API_KEY
|
||||
# Gate API Secret(实盘)
|
||||
GATE_API_SECRET=REPLACE_WITH_GATE_API_SECRET
|
||||
# Gate API(仅服务器 .env 配置;新机保持为空,填真钥后重启;错误密钥反复请求易导致 Gate 封 IP)
|
||||
GATE_API_KEY=
|
||||
GATE_API_SECRET=
|
||||
# 保证金模式:cross=全仓,isolated=逐仓
|
||||
GATE_TD_MODE=cross
|
||||
# 持仓筛选:hedge=双向持仓下按多空腿过滤;其它值(如 single)不按腿过滤
|
||||
|
||||
@@ -35,6 +35,11 @@ import sys
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
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_review_lib import (
|
||||
build_journal_ai_chart_path,
|
||||
@@ -346,8 +351,8 @@ def _resolve_app_tz():
|
||||
|
||||
APP_TZ = _resolve_app_tz()
|
||||
LIVE_TRADING_ENABLED = os.getenv("LIVE_TRADING_ENABLED", "false").lower() == "true"
|
||||
GATE_API_KEY = (os.getenv("GATE_API_KEY") or "").strip()
|
||||
GATE_API_SECRET = (os.getenv("GATE_API_SECRET") or "").strip()
|
||||
GATE_API_KEY = normalize_api_credential(os.getenv("GATE_API_KEY"))
|
||||
GATE_API_SECRET = normalize_api_credential(os.getenv("GATE_API_SECRET"))
|
||||
GATE_TD_MODE = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
|
||||
GATE_POS_MODE = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
|
||||
# 永续仓位止盈止损触发单: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.secret = GATE_API_SECRET
|
||||
MARKETS_LOADED = False
|
||||
# 鉴权失败后停止私有 API,避免坏钥反复签名;Gate 尤其易封 IP
|
||||
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||
ACCOUNT_BALANCE_CACHE = {
|
||||
"updated_at": 0.0,
|
||||
"funding_usdt": None,
|
||||
@@ -2547,6 +2554,8 @@ def enrich_order_item(raw_item, current_capital):
|
||||
|
||||
|
||||
def ensure_exchange_live_ready():
|
||||
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||
if not LIVE_TRADING_ENABLED:
|
||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||
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():
|
||||
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等."""
|
||||
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||
return False
|
||||
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):
|
||||
usdt_info = balance.get("USDT", {}) 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"]
|
||||
try:
|
||||
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
|
||||
try:
|
||||
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["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):
|
||||
global 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
|
||||
|
||||
|
||||
|
||||
@@ -80,12 +80,13 @@ TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
|
||||
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
|
||||
LIVE_TRADING_ENABLED=true
|
||||
# =============================================================================
|
||||
# OKX 账户 API(永续 + 期权共用同一套密钥;修改后须重启 PM2)
|
||||
# OKX 账户 API(永续+期权共用;仅服务器 .env 手改,前端不展示)
|
||||
# 新机保持为空;填真钥后 pm2 restart --update-env(含子代理)
|
||||
# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填
|
||||
# =============================================================================
|
||||
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
|
||||
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
|
||||
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
|
||||
OKX_API_KEY=
|
||||
OKX_API_SECRET=
|
||||
OKX_API_PASSPHRASE=
|
||||
# 保证金模式:cross=全仓,isolated=逐仓
|
||||
OKX_TD_MODE=cross
|
||||
# 持仓模式:hedge=双向持仓,net=单向净持仓
|
||||
|
||||
@@ -35,6 +35,11 @@ import sys
|
||||
if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
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_review_lib import (
|
||||
build_journal_ai_chart_path,
|
||||
@@ -360,9 +365,9 @@ def _promote_legacy_options_api_keys() -> None:
|
||||
|
||||
|
||||
_promote_legacy_options_api_keys()
|
||||
OKX_API_KEY = os.getenv("OKX_API_KEY", "")
|
||||
OKX_API_SECRET = os.getenv("OKX_API_SECRET", "")
|
||||
OKX_API_PASSPHRASE = os.getenv("OKX_API_PASSPHRASE", "")
|
||||
OKX_API_KEY = normalize_api_credential(os.getenv("OKX_API_KEY"))
|
||||
OKX_API_SECRET = normalize_api_credential(os.getenv("OKX_API_SECRET"))
|
||||
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_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()
|
||||
@@ -506,6 +511,7 @@ if OKX_API_KEY and OKX_API_SECRET and OKX_API_PASSPHRASE:
|
||||
exchange_options.password = OKX_API_PASSPHRASE
|
||||
|
||||
MARKETS_LOADED = False
|
||||
EXCHANGE_AUTH_DISABLED_MSG = ""
|
||||
ACCOUNT_BALANCE_CACHE = {
|
||||
"updated_at": 0.0,
|
||||
"funding_usdt": None,
|
||||
@@ -2485,6 +2491,8 @@ def enrich_order_item(raw_item, current_capital):
|
||||
|
||||
|
||||
def ensure_okx_live_ready():
|
||||
if EXCHANGE_AUTH_DISABLED_MSG:
|
||||
return False, EXCHANGE_AUTH_DISABLED_MSG
|
||||
if not LIVE_TRADING_ENABLED:
|
||||
return False, "未开启实盘下单(LIVE_TRADING_ENABLED=false)"
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
usdt_info = balance.get("USDT", {}) 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["trading_usdt"] = trading
|
||||
ACCOUNT_BALANCE_CACHE["updated_at"] = now_ts
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
if is_exchange_auth_error(e):
|
||||
_disable_private_api_after_auth_error(e)
|
||||
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):
|
||||
global 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
|
||||
|
||||
|
||||
@@ -2985,6 +3018,8 @@ def _okx_place_tp_sl_orders(exchange_symbol, direction, amount, stop_loss, take_
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -40,9 +40,9 @@ bash /opt/crypto_monitor/deploy/manage.sh
|
||||
|
||||
- 登录账号: **admin**
|
||||
- 登录密码: **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` 等变量名 |
|
||||
| **账户密码不进本页** | 登录用户名/密码在 **系统设置 → 账户密码修改** 中维护 |
|
||||
| **密钥自动托管** | 中控通信密钥,登录会话密钥由 **首次部署脚本自动生成并写入**(一次生成,不轮换),本页不提供编辑 |
|
||||
| **交易所 API 不进本页** | `OKX/BINANCE/GATE_API_*` 仅在服务器实例目录 `.env` 配置;新机默认为空,填真钥后 `pm2 restart --update-env` |
|
||||
| **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:关闭后顶栏隐藏 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`
|
||||
- 数据:`DB_PATH`,`UPLOAD_DIR`
|
||||
- **交易所 API**:`OKX_API_*`,`BINANCE_API_*`,`GATE_API_*`(仅 SSH;新机应为空)
|
||||
- 关键位门控:全部 `KEY_*`,`KLINE_*`
|
||||
- 轮询与同步:`BALANCE_REFRESH_SECONDS`,`PRICE_REFRESH_SECONDS`,`MONITOR_POLL_SECONDS`,`BREAKEVEN_*`,`RECONCILE_*`
|
||||
- 代理:`OKX_SOCKS_PROXY`,`BINANCE_HTTP_PROXY` 等
|
||||
@@ -212,7 +213,7 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT
|
||||
| 能力 | env 配置 | 系统设置 | 中控系统设置 |
|
||||
|------|----------|----------|--------------|
|
||||
| 登录用户名/密码 | ❌ | ✅ 账户密码修改 | ✅ 中控账户密码 |
|
||||
| 交易所 API | ✅(各所自配) | ❌ | ❌ |
|
||||
| 交易所 API | ❌(仅服务器 `.env`) | ❌ | ❌ |
|
||||
| AI / OpenAI | ❌ | ❌ | ✅ AI 配置(同步三所) |
|
||||
| 导航/区块显示 | ❌ | ✅ 导航显示 | ✅ 显示与导航 |
|
||||
| 手动资金划转 | ❌ | ✅ 永续资金划转 | ❌ |
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ python3 scripts/bootstrap_deploy_secrets.py
|
||||
|
||||
## 4. 实例 env 配置页变更
|
||||
|
||||
三所 **env 配置** 页已 **移除「AI 复盘」卡片**.交易所 API,企业微信,交易执行等仍各所自配.
|
||||
三所 **env 配置** 页已 **移除「AI 复盘」卡片**.企业微信、交易执行等仍各所自配;**交易所 API 仅服务器 `.env`**,前端不再展示.
|
||||
|
||||
实例侧若通过 API 提交已移除的 AI 键,会被白名单过滤,不会写入.
|
||||
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@
|
||||
| 网页登录密码 | ✅ | 本区块 |
|
||||
| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成,中控与实例一致 |
|
||||
| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成,三所相同 |
|
||||
| 交易所 API | ❌ | 在 **env 配置** 页(各所自配) |
|
||||
| 交易所 API | ❌ | **仅服务器** 各所 `.env`(`*_API_KEY` 等;前端 env 页已移除) |
|
||||
| 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]]] = {
|
||||
"okx": [
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
||||
("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"),
|
||||
("OKX_API_SECRET", "API Secret", "账户 API(永续+期权共用)"),
|
||||
("OKX_API_PASSPHRASE", "API Passphrase", "OKX 必填"),
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||
("OKX_TD_MODE", "保证金模式", ""),
|
||||
("OKX_POS_MODE", "持仓模式", ""),
|
||||
("OKX_POSITION_INST_TYPE", "仓位查询类型", "如 SWAP"),
|
||||
@@ -34,17 +31,13 @@ _EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
|
||||
),
|
||||
],
|
||||
"binance": [
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
||||
("BINANCE_API_KEY", "API Key", "永续子账户"),
|
||||
("BINANCE_API_SECRET", "API Secret", "永续子账户"),
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||
("BINANCE_MARGIN_MODE", "保证金模式", ""),
|
||||
("BINANCE_POSITION_MODE", "持仓模式", ""),
|
||||
("BINANCE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
|
||||
],
|
||||
"gate": [
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
|
||||
("GATE_API_KEY", "API Key", "永续子账户"),
|
||||
("GATE_API_SECRET", "API Secret", "永续子账户"),
|
||||
("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程;API Key 请在服务器实例目录 .env 配置后重启"),
|
||||
("GATE_TD_MODE", "保证金模式", ""),
|
||||
("GATE_POS_MODE", "持仓模式", ""),
|
||||
("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,
|
||||
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,
|
||||
|
||||
@@ -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.assertNotIn("OKX_OPTIONS_API_KEY", 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"]]}]
|
||||
clean, errors = validate_env_updates(groups_v, {"TRADE_DIRECTION": "long_only"})
|
||||
|
||||
Reference in New Issue
Block a user