mirror of
https://github.com/OpenByteInc/QuantDinger.git
synced 2026-09-28 23:32:55 +08:00
173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
"""Quick trade exchange error normalization."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
FRIENDLY_ERROR_PATTERNS = [
|
|
(
|
|
re.compile(
|
|
r"HTTP\s*451|\b451\b|restricted location|service unavailable from a restricted location",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.regionRestricted",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"OKX_SWAP_ACCOUNT_MODE_REQUIRED|51010|current account mode|unsupported under current account mode",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.okxAccountMode",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"INSUFFICIENT[_ ]?AVAILABLE|insufficient.{0,20}(balance|margin|fund)|margin.{0,30}while available|not enough|资金不足",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.insufficientBalance",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"invalid.{0,10}size|invalid.{0,10}(qty|quantity|amount|volume)|Order size.{0,20}(too small|below|minimum)|MIN_NOTIONAL|too many decimals|170137",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.invalidSize",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"invalid.{0,10}price|price.{0,20}(deviate|deviation|exceed|out of range)",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.invalidPrice",
|
|
),
|
|
(
|
|
re.compile(r"rate.?limit|too many request|429|REQUEST_FREQUENCY", re.IGNORECASE),
|
|
"quickTrade.errorHints.rateLimit",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"(invalid|wrong|expired).{0,10}(api.?key|key|signature|sign)|NOT_LOGIN|UNAUTHORIZED|permission.{0,10}denied|IP.{0,20}(not|whitelist|restrict)",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.authError",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"reduce.?only|position.{0,20}(not exist|not found|side)|POSITION_NOT_EXIST",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.positionConflict",
|
|
),
|
|
(
|
|
re.compile(
|
|
r"timeout|timed? ?out|connect|ECONNREFUSED|SSL|ConnectionError|RemoteDisconnected",
|
|
re.IGNORECASE,
|
|
),
|
|
"quickTrade.errorHints.networkError",
|
|
),
|
|
(
|
|
re.compile(r"maintenance|unavailable|system.{0,10}(busy|error|upgrade)|suspend|暂停", re.IGNORECASE),
|
|
"quickTrade.errorHints.exchangeMaintenance",
|
|
),
|
|
]
|
|
|
|
|
|
def parse_trade_error_hint(error_str: str) -> str:
|
|
"""Return an i18n key hint for common exchange trading errors."""
|
|
text = str(error_str or "")
|
|
for pattern, hint_key in FRIENDLY_ERROR_PATTERNS:
|
|
if pattern.search(text):
|
|
return hint_key
|
|
return ""
|
|
|
|
|
|
def extract_request_ip_from_exchange_error(err: str) -> str:
|
|
"""Extract an exchange-reported request IP from an error string."""
|
|
match = re.search(r"Current request IP\s+([0-9a-fA-F.:]+)", str(err or ""), re.IGNORECASE)
|
|
return (match.group(1) or "").strip() if match else ""
|
|
|
|
|
|
def exchange_error_user_message(*, exchange_id: str, err: str) -> Dict[str, str]:
|
|
"""Map a raw exchange error to UI-friendly text and an optional i18n key."""
|
|
text = str(err or "").strip()
|
|
low = text.lower()
|
|
exchange = (exchange_id or "").strip().lower()
|
|
if not text:
|
|
return {"message": "", "hint_key": ""}
|
|
|
|
if "OKX_SWAP_ACCOUNT_MODE_REQUIRED" in text or "51010" in text:
|
|
return {
|
|
"message": "OKX_SWAP_ACCOUNT_MODE_REQUIRED",
|
|
"hint_key": "quickTrade.errorHints.okxAccountMode",
|
|
}
|
|
|
|
if "40018" in text or "invalid ip" in low:
|
|
request_ip = extract_request_ip_from_exchange_error(text)
|
|
if exchange == "bitget":
|
|
message = "Bitget rejected the API request because the current egress IP is not whitelisted."
|
|
else:
|
|
message = "The exchange rejected the API request because the current egress IP is not whitelisted."
|
|
if request_ip:
|
|
message = f"{message} Current request IP: {request_ip}."
|
|
return {
|
|
"message": message,
|
|
"hint_key": "quickTrade.errorHints.ipWhitelist",
|
|
"request_ip": request_ip,
|
|
}
|
|
|
|
if "balance_not_enough" in low or "not enough balance" in low:
|
|
return {
|
|
"message": "Insufficient balance. Check the spot and derivatives wallets, then retry with a smaller order or transfer funds.",
|
|
"hint_key": "quickTrade.errorHints.insufficientBalance",
|
|
}
|
|
|
|
if "account-frozen-balance-insufficient" in low or "balance is not enough, left" in low:
|
|
return {
|
|
"message": "Available spot balance is insufficient. Some funds may be frozen by open orders.",
|
|
"hint_key": "quickTrade.errorHints.insufficientBalance",
|
|
}
|
|
|
|
if "insufficient margin" in low:
|
|
return {
|
|
"message": "Insufficient margin. Reduce order size, adjust leverage, or transfer funds to the derivatives wallet.",
|
|
"hint_key": "quickTrade.errorHints.insufficientBalance",
|
|
}
|
|
|
|
return {"message": text[:500], "hint_key": parse_trade_error_hint(text)}
|
|
|
|
|
|
def merge_balance_leg_errors(
|
|
swap_bal: Dict[str, Any],
|
|
spot_bal: Dict[str, Any],
|
|
*,
|
|
exchange_id: str = "",
|
|
) -> Dict[str, str]:
|
|
"""Collect swap and spot leg errors into top-level API response fields."""
|
|
parts: List[str] = []
|
|
hint_keys: List[str] = []
|
|
request_ip = ""
|
|
for leg in (swap_bal, spot_bal):
|
|
if not isinstance(leg, dict):
|
|
continue
|
|
raw_err = str(leg.get("error") or "").strip()
|
|
if not raw_err:
|
|
continue
|
|
meta = exchange_error_user_message(exchange_id=exchange_id, err=raw_err)
|
|
if meta.get("message"):
|
|
parts.append(str(meta["message"]))
|
|
if meta.get("hint_key"):
|
|
hint_keys.append(str(meta["hint_key"]))
|
|
if meta.get("request_ip"):
|
|
request_ip = str(meta["request_ip"])
|
|
|
|
if not parts:
|
|
return {}
|
|
return {
|
|
"error": parts[0],
|
|
"errors": parts,
|
|
"error_hint_key": hint_keys[0] if hint_keys else "",
|
|
"request_ip": request_ip,
|
|
}
|