This commit is contained in:
薇薇安 2026-01-19 15:50:19 +08:00
parent fd661d11d4
commit 71c9a7fb02
2 changed files with 88 additions and 12 deletions

View File

@ -71,7 +71,9 @@ class BinanceClient:
# 持仓模式缓存(币安 U本位合约dualSidePosition=True => 对冲模式False => 单向模式 # 持仓模式缓存(币安 U本位合约dualSidePosition=True => 对冲模式False => 单向模式
self._dual_side_position: Optional[bool] = None self._dual_side_position: Optional[bool] = None
self._position_mode_checked_at: float = 0.0 self._position_mode_checked_at: float = 0.0
self._position_mode_ttl: float = 60.0 # 秒,避免频繁调用接口 # 你可能会在币安端切换“对冲/单向”模式TTL 过长会导致短时间内仍按旧模式下单(携带 positionSide
# 这里缩短到 10s兼顾及时性与接口频率。
self._position_mode_ttl: float = 10.0 # 秒,避免频繁调用接口
# 隐藏敏感信息只显示前4位和后4位 # 隐藏敏感信息只显示前4位和后4位
api_key_display = f"{self.api_key[:4]}...{self.api_key[-4:]}" if self.api_key and len(self.api_key) > 8 else self.api_key api_key_display = f"{self.api_key[:4]}...{self.api_key[-4:]}" if self.api_key and len(self.api_key) > 8 else self.api_key
@ -654,17 +656,21 @@ class BinanceClient:
exchange_info = await self.client.futures_exchange_info() exchange_info = await self.client.futures_exchange_info()
for s in exchange_info['symbols']: for s in exchange_info['symbols']:
if s['symbol'] == symbol: if s['symbol'] == symbol:
# 提取数量精度信息 # 提取数量/价格精度信息
quantity_precision = s.get('quantityPrecision', 8) quantity_precision = s.get('quantityPrecision', 8)
price_precision = s.get('pricePrecision', 8)
# 从filters中提取minQty、stepSize和minNotional # 从filters中提取 minQty/stepSize/minNotional/tickSize
min_qty = None min_qty = None
step_size = None step_size = None
min_notional = None min_notional = None
tick_size = None
for f in s.get('filters', []): for f in s.get('filters', []):
if f['filterType'] == 'LOT_SIZE': if f['filterType'] == 'LOT_SIZE':
min_qty = float(f.get('minQty', 0)) min_qty = float(f.get('minQty', 0))
step_size = float(f.get('stepSize', 0)) step_size = float(f.get('stepSize', 0))
elif f.get('filterType') == 'PRICE_FILTER':
tick_size = float(f.get('tickSize', 0) or 0)
elif f['filterType'] == 'MIN_NOTIONAL': elif f['filterType'] == 'MIN_NOTIONAL':
min_notional = float(f.get('notional', 0)) min_notional = float(f.get('notional', 0))
@ -688,8 +694,10 @@ class BinanceClient:
info = { info = {
'quantityPrecision': quantity_precision, 'quantityPrecision': quantity_precision,
'pricePrecision': price_precision,
'minQty': min_qty or 0, 'minQty': min_qty or 0,
'stepSize': step_size or 0, 'stepSize': step_size or 0,
'tickSize': tick_size or 0,
'minNotional': min_notional, 'minNotional': min_notional,
'maxLeverage': int(max_leverage_supported) # 交易对支持的最大杠杆 'maxLeverage': int(max_leverage_supported) # 交易对支持的最大杠杆
} }
@ -752,6 +760,36 @@ class BinanceClient:
return adjusted return adjusted
@staticmethod
def _adjust_price_to_tick(price: float, tick_size: float, side: str) -> float:
"""
LIMIT 价格调整为 tickSize 的整数倍避免
-4014 Price not increased by tick size
-1111 Precision is over the maximum defined for this asset部分情况下也会由 price 引发
规则入场限价
- BUY向下取整更便宜且不会买贵了
- SELL向上取整更贵/更高且不会卖便宜了
"""
try:
from decimal import Decimal, ROUND_DOWN, ROUND_UP
except Exception:
# fallback不用 Decimal 时,至少 round 到 8 位,尽量减少精度问题
return float(round(float(price), 8))
try:
p = Decimal(str(price))
t = Decimal(str(tick_size))
if t <= 0:
return float(p)
q = p / t
rounding = ROUND_DOWN if (side or "").upper() == "BUY" else ROUND_UP
q2 = q.to_integral_value(rounding=rounding)
p2 = q2 * t
return float(p2)
except Exception:
return float(round(float(price), 8))
def _adjust_quantity_precision_up(self, quantity: float, symbol_info: Dict) -> float: def _adjust_quantity_precision_up(self, quantity: float, symbol_info: Dict) -> float:
""" """
向上取整调整数量精度使其符合币安要求 向上取整调整数量精度使其符合币安要求
@ -1011,7 +1049,13 @@ class BinanceClient:
raise ValueError("限价单必须指定价格") raise ValueError("限价单必须指定价格")
params = dict(params) params = dict(params)
params['timeInForce'] = 'GTC' params['timeInForce'] = 'GTC'
params['price'] = price # LIMIT 价格按 tickSize 修正(避免 -4014 / -1111
tick = float(symbol_info.get("tickSize", 0) or 0) if symbol_info else 0.0
p2 = self._adjust_price_to_tick(float(price), tick, side)
# 用字符串提交,避免浮点造成的精度问题
params['price'] = str(p2)
# quantity 也转成字符串提交(同样避免浮点精度)
params['quantity'] = str(params.get('quantity'))
return await self.client.futures_create_order(**params) return await self.client.futures_create_order(**params)
# 提交订单;若遇到: # 提交订单;若遇到:
@ -1052,8 +1096,17 @@ class BinanceClient:
if error_code == -1111: if error_code == -1111:
logger.error(f"下单失败 {symbol} {side}: 精度错误 - {e}") logger.error(f"下单失败 {symbol} {side}: 精度错误 - {e}")
logger.error(f" 原始数量: {quantity}") logger.error(f" 原始数量: {quantity}")
if order_type == 'LIMIT':
logger.error(f" 原始价格: {price}")
if symbol_info: if symbol_info:
logger.error(f" 交易对精度: {symbol_info}") logger.error(f" 交易对精度: {symbol_info}")
elif error_code == -4014:
# Price not increased by tick size.
logger.error(f"下单失败 {symbol} {side}: 价格步长错误(-4014) - {e}")
logger.error(f" 原始数量: {quantity}")
logger.error(f" 原始价格: {price}")
if symbol_info:
logger.error(f" tickSize: {symbol_info.get('tickSize')}, pricePrecision: {symbol_info.get('pricePrecision')}")
elif error_code == -4164: elif error_code == -4164:
logger.error(f"下单失败 {symbol} {side}: 订单名义价值不足 - {e}") logger.error(f"下单失败 {symbol} {side}: 订单名义价值不足 - {e}")
logger.error(f" 订单名义价值必须至少为 5 USDT (除非选择 reduce only)") logger.error(f" 订单名义价值必须至少为 5 USDT (除非选择 reduce only)")
@ -1061,11 +1114,12 @@ class BinanceClient:
logger.error(f" 最小名义价值: {symbol_info.get('minNotional', 5.0)} USDT") logger.error(f" 最小名义价值: {symbol_info.get('minNotional', 5.0)} USDT")
elif error_code == -2022: elif error_code == -2022:
# ReduceOnly Order is rejected - 可能是没有持仓或持仓方向不对 # ReduceOnly Order is rejected - 可能是没有持仓或持仓方向不对
# 这类错误在并发/竞态场景很常见:我们以为还有仓位,但实际上已经被其他任务/手动操作平掉了
# 对于 reduce_only=True调用方应当把它当作“幂等平仓”的可接受结果再查一次实时持仓即可
if reduce_only:
logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)可能仓位已为0/方向腿不匹配),将由上层做幂等处理")
else:
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}") logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}")
logger.error(f" 可能的原因:")
logger.error(f" 1. 币安账户中没有该交易对的持仓")
logger.error(f" 2. 持仓方向与平仓方向不匹配")
logger.error(f" 3. 持仓数量不足")
elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower(): elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower():
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}") logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}")
logger.error(f" 错误码: {error_code}") logger.error(f" 错误码: {error_code}")

View File

@ -871,9 +871,21 @@ class PositionManager:
) )
return True return True
else: else:
# place_order 返回 None说明下单失败 # place_order 返回 None可能是 -2022ReduceOnly rejected等竞态场景
logger.error(f"{symbol} [平仓] ❌ 下单返回 None可能的原因") # 兜底再查一次实时持仓如果已经为0则当作“已平仓”处理避免刷屏与误判失败
logger.error(f" 1. 订单名义价值不足(小于最小要求)") try:
live2 = await self._get_live_position_amt(symbol, position_side=position_side)
except Exception:
live2 = None
if live2 is None or abs(live2) <= 0:
logger.warning(f"{symbol} [平仓] 下单返回None但实时持仓已为0按已平仓处理可能竞态/手动平仓)")
await self._stop_position_monitoring(symbol)
if symbol in self.active_positions:
del self.active_positions[symbol]
return True
logger.error(f"{symbol} [平仓] ❌ 下单返回 None实时持仓仍存在: {live2}),可能的原因:")
logger.error(f" 1. ReduceOnly 被拒绝(-2022但持仓未同步")
logger.error(f" 2. 数量精度调整后为 0 或负数") logger.error(f" 2. 数量精度调整后为 0 或负数")
logger.error(f" 3. 无法获取价格信息") logger.error(f" 3. 无法获取价格信息")
logger.error(f" 4. 其他下单错误(已在 place_order 中记录)") logger.error(f" 4. 其他下单错误(已在 place_order 中记录)")
@ -1248,6 +1260,16 @@ class PositionManager:
logger.info(f"{symbol} 剩余仓位止损移至成本价(保本),配合移动止损博取更大利润") logger.info(f"{symbol} 剩余仓位止损移至成本价(保本),配合移动止损博取更大利润")
else: else:
logger.info(f"{symbol} 已部分止盈,但已关闭移动止损:不自动将止损移至成本价") logger.info(f"{symbol} 已部分止盈,但已关闭移动止损:不自动将止损移至成本价")
else:
# 兜底:可能遇到 -2022reduceOnly rejected等竞态重新查一次持仓
try:
live2 = await self._get_live_position_amt(symbol, position_side=close_position_side)
except Exception:
live2 = None
if live2 is None or abs(live2) <= 0:
logger.warning(f"{symbol} 部分止盈下单返回None但实时持仓已为0跳过")
continue
logger.warning(f"{symbol} 部分止盈下单返回None实时持仓仍存在: {live2}),稍后将继续由止损/止盈逻辑处理")
except Exception as e: except Exception as e:
logger.error(f"{symbol} 部分止盈失败: {e}") logger.error(f"{symbol} 部分止盈失败: {e}")