feat(spot_order): 增强现货下单API的错误处理与文档说明

在现货下单API中添加了对下单金额的最小限制(5 USDT),并改进了错误处理机制,针对不同的Binance API异常提供了详细的错误信息。更新了API文档说明,确保用户能够更清晰地理解下单逻辑与要求。此改动提升了系统的健壮性与用户体验。
This commit is contained in:
薇薇安 2026-02-25 09:26:29 +08:00
parent cbba86001a
commit 163b8303ec
5 changed files with 1855 additions and 52 deletions

View File

@ -1583,12 +1583,21 @@ async def place_spot_order(
price: float = Query(None, description="限价单价格LIMIT 时必填)"),
account_id: int = Depends(get_account_id),
):
"""现货一键下单(市价单或限价单)。仅支持当前账号 API 密钥对应的现货账户。"""
"""
现货一键下单参考文档POST /api/v3/order现货
市价单使用 quoteOrderQty 表示要花费/收到的 USDT 数量限价单使用 quantity+price
"""
try:
if side not in ("BUY", "SELL"):
raise HTTPException(status_code=400, detail="side 必须是 BUY 或 SELL")
if quote_order_qty <= 0:
raise HTTPException(status_code=400, detail="下单金额必须大于 0")
# 币安现货多数交易对 MIN_NOTIONAL 为 5 USDT
if quote_order_qty < 5:
raise HTTPException(
status_code=400,
detail="现货下单金额不能低于 5 USDT满足 MIN_NOTIONAL请调大默认下单金额",
)
if order_type.upper() == "LIMIT" and (price is None or price <= 0):
raise HTTPException(status_code=400, detail="限价单必须填写 price")
@ -1598,27 +1607,32 @@ async def place_spot_order(
try:
from binance_client import BinanceClient
from binance.exceptions import BinanceAPIException
except ImportError:
trading_system_path = project_root / "trading_system"
sys.path.insert(0, str(trading_system_path))
from binance_client import BinanceClient
from binance.exceptions import BinanceAPIException
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
await client.connect()
try:
# 现货使用底层 AsyncClient 的 create_orderspot API
# 现货接口POST /api/v3/order与合约 fapi 不同。python-binance 中 create_order 为现货。
# 文档:市价单可用 quoteOrderQty 设置 quote asset 数量,传字符串避免精度问题。
quote_str = f"{quote_order_qty:.2f}"
if order_type.upper() == "MARKET":
order = await client.client.create_order(
symbol=symbol,
symbol=symbol.strip().upper(),
side=side,
type="MARKET",
quoteOrderQty=quote_order_qty,
quoteOrderQty=quote_str,
)
else:
# LIMIT: 需要 quantity用 quote_order_qty / price 估算
qty = quote_order_qty / price
# 限价单需要 quantity + pricequantity 需符合交易对 LOT_SIZE
order = await client.client.create_order(
symbol=symbol,
symbol=symbol.strip().upper(),
side=side,
type="LIMIT",
timeInForce="GTC",
@ -1636,6 +1650,19 @@ async def place_spot_order(
await client.disconnect()
except HTTPException:
raise
except BinanceAPIException as e:
code = getattr(e, "code", getattr(e, "status_code", ""))
msg = str(e).strip() or getattr(e, "message", "")
if code == -2015:
detail = "API Key 无现货交易权限或无效。请在币安 API 管理中勾选「启用现货与杠杆交易」并使用该 Key。"
elif code == -1013:
detail = f"订单不满足交易对规则(如 MIN_NOTIONAL 约 5 USDT: {msg}"
elif code == -1121:
detail = f"交易对格式无效: {msg}"
else:
detail = f"币安现货接口错误 (code={code}): {msg}"
logger.warning("现货下单 BinanceAPIException: %s", detail)
raise HTTPException(status_code=400, detail=detail)
except Exception as e:
logger.exception("现货下单失败: %s", e)
raise HTTPException(status_code=500, detail=f"现货下单失败: {str(e)}")

1784
docs/bian/现货交易.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -714,7 +714,10 @@ class MarketScanner:
logger.debug(f"{symbol} 技术指标计算结果已缓存 (TTL: 30秒)")
except Exception as e:
logger.debug(f"{symbol} 缓存技术指标计算结果失败: {e}")
# 统一:缓存可能返回 None保证下游始终拿到 'trending'/'ranging'/'unknown'
market_regime = market_regime or 'unknown'
# 计算交易信号得分(用于排序)- 保留用于兼容性
signal_score = 0

View File

@ -85,7 +85,7 @@ class RedisCache:
self._connected = False
async def connect(self):
"""连接 Redis"""
"""连接 Redis。若已连接且 ping 正常则直接返回,避免重复连接和刷屏日志。"""
if not REDIS_AVAILABLE:
import sys
import os
@ -97,96 +97,85 @@ class RedisCache:
self.redis = None
self._connected = False
return
# 已连接且连接仍有效时不再重复连接,避免多模块多次 connect 时刷屏
if self._connected and self.redis:
try:
await self.redis.ping()
return
except Exception:
self._connected = False
self.redis = None
try:
# 构建连接参数
connection_kwargs = {}
# 如果使用 TLSredis-py的异步客户端使用特定的SSL参数而不是ssl上下文
if self.use_tls or self.redis_url.startswith('rediss://'):
# redis-py的异步客户端不支持直接传递ssl上下文
# 而是使用ssl_cert_reqs、ssl_ca_certs等参数
# 当URL是rediss://时redis-py会自动启用SSL
# 设置证书验证要求(字符串格式:'required', 'optional', 'none'
connection_kwargs['ssl_cert_reqs'] = self.ssl_cert_reqs
# 如果提供了 CA 证书路径
if self.ssl_ca_certs:
connection_kwargs['ssl_ca_certs'] = self.ssl_ca_certs
# 设置主机名验证根据ssl_cert_reqs自动设置但可以显式指定
if self.ssl_cert_reqs == 'none':
connection_kwargs['ssl_check_hostname'] = False
elif self.ssl_cert_reqs == 'required':
connection_kwargs['ssl_check_hostname'] = True
else: # optional
else:
connection_kwargs['ssl_check_hostname'] = False
logger.info(f"使用 TLS 连接 Redis: {self.redis_url} (ssl_cert_reqs={self.ssl_cert_reqs})")
logger.debug("使用 TLS 连接 Redis: %s (ssl_cert_reqs=%s)", self.redis_url, self.ssl_cert_reqs)
# 如果 URL 中不包含用户名和密码,且提供了独立的用户名和密码参数,则添加到连接参数中
# 注意:如果 URL 中已经包含认证信息(如 redis://user:pass@host:port则优先使用 URL 中的
if self.username and self.password:
# 检查 URL 中是否已包含认证信息格式redis://user:pass@host:port
url_parts = self.redis_url.split('://')
if len(url_parts) == 2:
url_after_scheme = url_parts[1]
# 如果 URL 中不包含 @ 符号,说明没有在 URL 中指定认证信息
if '@' not in url_after_scheme:
# URL 中不包含认证信息,使用独立的用户名和密码参数
connection_kwargs['username'] = self.username
connection_kwargs['password'] = self.password
logger.info(f"使用独立的用户名和密码进行认证: {self.username}")
logger.debug("使用独立的用户名和密码进行认证(不记录用户名)")
else:
logger.info("URL 中已包含认证信息,优先使用 URL 中的认证信息")
logger.debug("URL 中已包含认证信息,优先使用 URL 中的认证信息")
else:
# URL 格式异常,尝试使用独立的用户名和密码
connection_kwargs['username'] = self.username
connection_kwargs['password'] = self.password
logger.info(f"URL 格式异常,使用独立的用户名和密码进行认证: {self.username}")
# 验证URL格式redis-py要求URL必须有正确的scheme
if not self.redis_url or not any(self.redis_url.startswith(scheme) for scheme in ['redis://', 'rediss://', 'unix://']):
logger.debug("URL 格式异常,使用独立的用户名和密码进行认证")
# 验证URL格式
if not self.redis_url or not any(self.redis_url.startswith(s) for s in ['redis://', 'rediss://', 'unix://']):
raise ValueError(
f"Redis URL必须指定以下scheme之一 (redis://, rediss://, unix://): {self.redis_url}"
)
# 创建 Redis 连接
# 使用 redis.asyncio.from_urlredis-py 4.2+)或 aioredis.from_url向后兼容
# redis-py 需要 decode_responses=Trueaioredis 2.0 可能不需要
# 检查是否是 redis.asyncio通过检查模块名称
try:
module_name = aioredis.__name__ if hasattr(aioredis, '__name__') else ''
is_redis_py = 'redis.asyncio' in module_name or 'redis' in module_name
except:
except Exception:
is_redis_py = False
if is_redis_py:
# redis-py 4.2+ 的异步客户端,需要 decode_responses=True
connection_kwargs['decode_responses'] = True
# aioredis 2.0 不需要 decode_responses它默认返回字节
self.redis = await aioredis.from_url(
self.redis_url,
**connection_kwargs
)
# 测试连接
await self.redis.ping()
self._connected = True
logger.info(f"✓ Redis 连接成功: {self.redis_url}")
logger.info("✓ Redis 连接成功: %s", self.redis_url)
except Exception as e:
logger.warning(f"Redis 连接失败: {e},将使用内存缓存")
self.redis = None
self._connected = False
logger.warning("Redis 连接失败: %s,将使用内存缓存", e)
if self.redis:
try:
await self.redis.close()
except:
except Exception:
pass
self.redis = None
self.redis = None
self._connected = False
async def get(self, key: str) -> Optional[Any]:
"""

View File

@ -124,8 +124,8 @@ class TradingStrategy:
symbol = symbol_info['symbol']
change_percent = symbol_info['changePercent']
direction = symbol_info['direction']
market_regime = symbol_info.get('marketRegime', 'unknown')
market_regime = symbol_info.get('marketRegime') or 'unknown'
# unknown 表示 K 线不足或未判定趋势/震荡,属正常;仅生成推荐,不满足 ONLY_TRENDING 时不自动下单
logger.info(
f"处理交易对: {symbol} "
f"({direction} {change_percent:.2f}%, 市场状态: {market_regime})"