From 71c9a7fb0289568afe693d5dbe57680779c855d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Mon, 19 Jan 2026 15:50:19 +0800 Subject: [PATCH] a --- trading_system/binance_client.py | 72 ++++++++++++++++++++++++++---- trading_system/position_manager.py | 28 ++++++++++-- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 7eb15d2..fb1d3cd 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -71,7 +71,9 @@ class BinanceClient: # 持仓模式缓存(币安 U本位合约):dualSidePosition=True => 对冲模式;False => 单向模式 self._dual_side_position: Optional[bool] = None 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位 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() for s in exchange_info['symbols']: if s['symbol'] == symbol: - # 提取数量精度信息 + # 提取数量/价格精度信息 quantity_precision = s.get('quantityPrecision', 8) + price_precision = s.get('pricePrecision', 8) - # 从filters中提取minQty、stepSize和minNotional + # 从filters中提取 minQty/stepSize/minNotional/tickSize min_qty = None step_size = None min_notional = None + tick_size = None for f in s.get('filters', []): if f['filterType'] == 'LOT_SIZE': min_qty = float(f.get('minQty', 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': min_notional = float(f.get('notional', 0)) @@ -688,8 +694,10 @@ class BinanceClient: info = { 'quantityPrecision': quantity_precision, + 'pricePrecision': price_precision, 'minQty': min_qty or 0, 'stepSize': step_size or 0, + 'tickSize': tick_size or 0, 'minNotional': min_notional, 'maxLeverage': int(max_leverage_supported) # 交易对支持的最大杠杆 } @@ -751,6 +759,36 @@ class BinanceClient: logger.info(f"数量精度调整: {quantity} -> {adjusted} (精度: {quantity_precision}, stepSize: {step_size}, minQty: {min_qty})") 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: """ @@ -1011,7 +1049,13 @@ class BinanceClient: raise ValueError("限价单必须指定价格") params = dict(params) 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) # 提交订单;若遇到: @@ -1052,8 +1096,17 @@ class BinanceClient: if error_code == -1111: logger.error(f"下单失败 {symbol} {side}: 精度错误 - {e}") logger.error(f" 原始数量: {quantity}") + if order_type == 'LIMIT': + logger.error(f" 原始价格: {price}") if 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: logger.error(f"下单失败 {symbol} {side}: 订单名义价值不足 - {e}") logger.error(f" 订单名义价值必须至少为 5 USDT (除非选择 reduce only)") @@ -1061,11 +1114,12 @@ class BinanceClient: logger.error(f" 最小名义价值: {symbol_info.get('minNotional', 5.0)} USDT") elif error_code == -2022: # ReduceOnly Order is rejected - 可能是没有持仓或持仓方向不对 - logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}") - logger.error(f" 可能的原因:") - logger.error(f" 1. 币安账户中没有该交易对的持仓") - logger.error(f" 2. 持仓方向与平仓方向不匹配") - logger.error(f" 3. 持仓数量不足") + # 这类错误在并发/竞态场景很常见:我们以为还有仓位,但实际上已经被其他任务/手动操作平掉了 + # 对于 reduce_only=True:调用方应当把它当作“幂等平仓”的可接受结果(再查一次实时持仓即可)。 + if reduce_only: + logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)(可能仓位已为0/方向腿不匹配),将由上层做幂等处理") + else: + logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}") elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower(): logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}") logger.error(f" 错误码: {error_code}") diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 39b784b..c46fdb8 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -871,9 +871,21 @@ class PositionManager: ) return True else: - # place_order 返回 None,说明下单失败 - logger.error(f"{symbol} [平仓] ❌ 下单返回 None,可能的原因:") - logger.error(f" 1. 订单名义价值不足(小于最小要求)") + # place_order 返回 None:可能是 -2022(ReduceOnly rejected)等竞态场景 + # 兜底:再查一次实时持仓,如果已经为0,则当作“已平仓”处理,避免刷屏与误判失败 + 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" 3. 无法获取价格信息") logger.error(f" 4. 其他下单错误(已在 place_order 中记录)") @@ -1248,6 +1260,16 @@ class PositionManager: logger.info(f"{symbol} 剩余仓位止损移至成本价(保本),配合移动止损博取更大利润") else: 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: logger.error(f"{symbol} 部分止盈失败: {e}")