feat(spot_order): 增强现货下单API的错误处理与文档说明
在现货下单API中添加了对下单金额的最小限制(5 USDT),并改进了错误处理机制,针对不同的Binance API异常提供了详细的错误信息。更新了API文档说明,确保用户能够更清晰地理解下单逻辑与要求。此改动提升了系统的健壮性与用户体验。
This commit is contained in:
parent
cbba86001a
commit
163b8303ec
|
|
@ -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_order(spot 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 + price,quantity 需符合交易对 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
1784
docs/bian/现货交易.txt
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
||||
# 如果使用 TLS(redis-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_url(redis-py 4.2+)或 aioredis.from_url(向后兼容)
|
||||
# redis-py 需要 decode_responses=True,aioredis 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]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user