a
This commit is contained in:
parent
fd661d11d4
commit
71c9a7fb02
|
|
@ -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 - 可能是没有持仓或持仓方向不对
|
||||||
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}")
|
# 这类错误在并发/竞态场景很常见:我们以为还有仓位,但实际上已经被其他任务/手动操作平掉了
|
||||||
logger.error(f" 可能的原因:")
|
# 对于 reduce_only=True:调用方应当把它当作“幂等平仓”的可接受结果(再查一次实时持仓即可)。
|
||||||
logger.error(f" 1. 币安账户中没有该交易对的持仓")
|
if reduce_only:
|
||||||
logger.error(f" 2. 持仓方向与平仓方向不匹配")
|
logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)(可能仓位已为0/方向腿不匹配),将由上层做幂等处理")
|
||||||
logger.error(f" 3. 持仓数量不足")
|
else:
|
||||||
|
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}")
|
||||||
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}")
|
||||||
|
|
|
||||||
|
|
@ -871,9 +871,21 @@ class PositionManager:
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# place_order 返回 None,说明下单失败
|
# place_order 返回 None:可能是 -2022(ReduceOnly 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:
|
||||||
|
# 兜底:可能遇到 -2022(reduceOnly 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}")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user