Files
DinQuant/backend_api_python/app/services/exchange_execution.py
T
Dinger 7626328c9c feat(backend): encrypt exchange credentials, remove IS_DEMO_MODE
- Fernet encrypt qd_exchange_credentials via SECRET_KEY (cryptography)

- Remove global read-only demo middleware; drop is_demo from auth payloads

- Egress whitelist: /api/credentials/egress-ip returns ipv4 + ipv6 (ipify)

- Exchange factory: demo/testnet URLs and OKX simulated-trading header

- Bitget spot connection test; misc route/service fixes

Made-with: Cursor
2026-03-24 19:10:18 +08:00

150 lines
4.7 KiB
Python

"""
Exchange execution helpers (local deployment).
This module provides helpers for resolving exchange configs and safe logging.
Notes:
- In paper mode, the system only enqueues signals into `pending_orders`.
- Real trading execution is intentionally not implemented here.
"""
from __future__ import annotations
import json
from typing import Any, Dict
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.credential_crypto import decrypt_credential_blob
logger = get_logger(__name__)
def _safe_json_loads(value: Any, default: Any) -> Any:
if value is None:
return default
if isinstance(value, (dict, list)):
return value
if not isinstance(value, str):
return default
s = value.strip()
if not s:
return default
try:
return json.loads(s)
except Exception:
return default
def mask_secret(s: str, keep: int = 4) -> str:
"""Return a masked representation of a secret for safe logs."""
if not s:
return ""
s = str(s)
if len(s) <= keep * 2:
return s[: max(1, keep)] + "***"
return f"{s[:keep]}...{s[-keep:]}"
def safe_exchange_config_for_log(cfg: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(cfg, dict):
return {}
out = dict(cfg)
for k in ["api_key", "secret_key", "passphrase", "apiKey", "secret", "password"]:
if k in out and out.get(k):
out[k] = mask_secret(str(out.get(k)))
return out
def load_strategy_configs(strategy_id: int) -> Dict[str, Any]:
"""Load strategy config fields needed for live execution."""
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, user_id, exchange_config, trading_config, market_type, leverage, execution_mode, market_category
FROM qd_strategies_trading
WHERE id = %s
""",
(int(strategy_id),),
)
row = cur.fetchone() or {}
cur.close()
exchange_config = _safe_json_loads(row.get("exchange_config"), {})
trading_config = _safe_json_loads(row.get("trading_config"), {})
market_type = (row.get("market_type") or exchange_config.get("market_type") or "swap").strip()
leverage = float(row.get("leverage") or trading_config.get("leverage") or exchange_config.get("leverage") or 1.0)
execution_mode = (row.get("execution_mode") or "signal").strip().lower()
market_category = (row.get("market_category") or "Crypto").strip()
user_id = int(row.get("user_id") or 1)
return {
"strategy_id": int(strategy_id),
"user_id": user_id,
"exchange_config": exchange_config if isinstance(exchange_config, dict) else {},
"trading_config": trading_config if isinstance(trading_config, dict) else {},
"market_type": market_type,
"leverage": leverage,
"execution_mode": execution_mode,
"market_category": market_category,
}
def _load_credential_config(credential_id: int, user_id: int = 1) -> Dict[str, Any]:
"""Load credential JSON from qd_exchange_credentials (Fernet via SECRET_KEY)."""
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT encrypted_config
FROM qd_exchange_credentials
WHERE id = %s AND user_id = %s
""",
(int(credential_id), int(user_id)),
)
row = cur.fetchone() or {}
cur.close()
raw = row.get("encrypted_config")
try:
plain = decrypt_credential_blob(raw)
except ValueError as e:
logger.warning(f"decrypt credential_id={credential_id}: {e}")
return {}
return _safe_json_loads(plain, {}) or {}
def resolve_exchange_config(exchange_config: Dict[str, Any], user_id: int = 1) -> Dict[str, Any]:
"""
Resolve exchange config.
Supports:
- direct inline config: {exchange_id, api_key, secret_key, passphrase?}
- credential reference: {credential_id: 123, ...overrides}
"""
if not isinstance(exchange_config, dict):
return {}
merged: Dict[str, Any] = {}
credential_id = exchange_config.get("credential_id") or exchange_config.get("credentials_id")
try:
if credential_id:
base = _load_credential_config(int(credential_id), user_id=user_id)
if isinstance(base, dict):
merged.update(base)
except Exception as e:
logger.warning(f"Failed to load credential_id={credential_id}: {e}")
# Overlay strategy-level settings (non-empty wins)
for k, v in exchange_config.items():
if v is None:
continue
if isinstance(v, str) and not v.strip():
continue
merged[k] = v
return merged