diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index ba4818b..abadcd4 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -143,20 +143,11 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str): except Exception: raise HTTPException(status_code=400, detail=f"{symbol} 数据库缺少止损/止盈价,且无法回退计算,无法补挂") - # 5) 取消旧的保护单,避免重复 + # 5) 取消旧的保护单(Algo 条件单),避免重复 try: - orders = await client.client.futures_get_open_orders(symbol=symbol) - for o in orders or []: - try: - if not isinstance(o, dict): - continue - otype = str(o.get("type") or "").upper() - if otype in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}: - oid = o.get("orderId") - if oid: - await client.client.futures_cancel_order(symbol=symbol, orderId=int(oid)) - except Exception: - continue + await client.cancel_open_algo_orders_by_order_types( + symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"} + ) except Exception: pass @@ -198,19 +189,22 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str): tp_round = "DOWN" if side == "BUY" else "UP" close_side = "SELL" if side == "BUY" else "BUY" + # Algo 条件单使用 triggerPrice(不是 stopPrice) sl_params = { + "algoType": "CONDITIONAL", "symbol": symbol, "side": close_side, "type": "STOP_MARKET", - "stopPrice": _fmt(sl_price, sl_round), + "triggerPrice": _fmt(sl_price, sl_round), "closePosition": True, "workingType": "MARK_PRICE", } tp_params = { + "algoType": "CONDITIONAL", "symbol": symbol, "side": close_side, "type": "TAKE_PROFIT_MARKET", - "stopPrice": _fmt(tp_price, tp_round), + "triggerPrice": _fmt(tp_price, tp_round), "closePosition": True, "workingType": "MARK_PRICE", } @@ -218,31 +212,30 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str): sl_params["positionSide"] = "LONG" if side == "BUY" else "SHORT" tp_params["positionSide"] = "LONG" if side == "BUY" else "SHORT" - sl_order = await client.client.futures_create_order(**sl_params) - tp_order = await client.client.futures_create_order(**tp_params) + sl_order = await client.futures_create_algo_order(sl_params) + tp_order = await client.futures_create_algo_order(tp_params) # 再查一次未成交委托,确认是否真的挂上(并用于前端展示/排查) open_orders = [] try: - oo = await client.client.futures_get_open_orders(symbol=symbol) + oo = await client.futures_get_open_algo_orders(symbol=symbol, algo_type="CONDITIONAL") if isinstance(oo, list): for o in oo: try: if not isinstance(o, dict): continue - otype2 = str(o.get("type") or "").upper() - if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}: + otype2 = str(o.get("orderType") or o.get("type") or "").upper() + if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET", "STOP", "TAKE_PROFIT"}: open_orders.append( { - "orderId": o.get("orderId"), - "type": otype2, + "algoId": o.get("algoId"), + "orderType": otype2, "side": o.get("side"), - "stopPrice": o.get("stopPrice"), - "price": o.get("price"), + "triggerPrice": o.get("triggerPrice"), "workingType": o.get("workingType"), "positionSide": o.get("positionSide"), "closePosition": o.get("closePosition"), - "status": o.get("status"), + "algoStatus": o.get("algoStatus"), "updateTime": o.get("updateTime"), } ) diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index ed615dd..9518e84 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -1298,6 +1298,45 @@ class BinanceClient: logger.error(f"取消订单失败: {e}") return False + # ========================= + # Algo Orders(条件单/止盈止损/计划委托) + # 说明:币安在 2025-12 后将 USDT-M 合约的 STOP/TP/Trailing 等条件单迁移到 Algo 接口: + # - POST /fapi/v1/algoOrder + # - GET /fapi/v1/openAlgoOrders + # - DELETE /fapi/v1/algoOrder + # 如果仍用 /fapi/v1/order 下 STOP_MARKET/TAKE_PROFIT_MARKET + closePosition 会报 -4120。 + # ========================= + + async def futures_create_algo_order(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]: + try: + # python-binance 内部会自动补 timestamp / signature + res = await self.client._request_futures_api("post", "algoOrder", True, data=params) + return res if isinstance(res, dict) else None + except Exception as e: + logger.warning(f"{params.get('symbol')} 创建 Algo 条件单失败: {e}") + return None + + async def futures_get_open_algo_orders(self, symbol: Optional[str] = None, algo_type: str = "CONDITIONAL") -> List[Dict[str, Any]]: + try: + data: Dict[str, Any] = {} + if symbol: + data["symbol"] = symbol + if algo_type: + data["algoType"] = algo_type + res = await self.client._request_futures_api("get", "openAlgoOrders", True, data=data) + return res if isinstance(res, list) else [] + except Exception as e: + logger.debug(f"{symbol or ''} 获取 openAlgoOrders 失败: {e}") + return [] + + async def futures_cancel_algo_order(self, algo_id: int) -> bool: + try: + _ = await self.client._request_futures_api("delete", "algoOrder", True, data={"algoId": int(algo_id)}) + return True + except Exception as e: + logger.debug(f"取消 Algo 条件单失败 algoId={algo_id}: {e}") + return False + async def get_open_orders(self, symbol: str) -> List[Dict]: """ 获取某交易对的未成交委托(用于防止重复挂保护单)。 @@ -1336,6 +1375,33 @@ class BinanceClient: except Exception: return 0 + async def cancel_open_algo_orders_by_order_types(self, symbol: str, order_types: set[str]) -> int: + """ + 取消指定类型的“Algo 条件单”(openAlgoOrders)。 + 返回取消数量。 + """ + try: + want = {str(t).upper() for t in (order_types or set())} + if not want: + return 0 + orders = await self.futures_get_open_algo_orders(symbol=symbol, algo_type="CONDITIONAL") + cancelled = 0 + for o in orders or []: + try: + if not isinstance(o, dict): + continue + otype = str(o.get("orderType") or o.get("type") or "").upper() + algo_id = o.get("algoId") + if algo_id and otype in want: + ok = await self.futures_cancel_algo_order(int(algo_id)) + if ok: + cancelled += 1 + except Exception: + continue + return cancelled + except Exception: + return 0 + async def place_trigger_close_position_order( self, symbol: str, @@ -1422,32 +1488,34 @@ class BinanceClient: stop_price_str = self._format_price_str_with_rounding(sp, symbol_info, rounding_mode) + # Algo 条件单接口使用 triggerPrice(不是 stopPrice) params: Dict[str, Any] = { + "algoType": "CONDITIONAL", "symbol": symbol, "side": close_side, "type": ttype, - "stopPrice": stop_price_str, - "closePosition": True, + "triggerPrice": stop_price_str, "workingType": working_type, + "closePosition": True, } # 对冲模式:必须指定 positionSide if dual is True: params["positionSide"] = "LONG" if pd == "BUY" else "SHORT" - try: - return await self.client.futures_create_order(**params) - except BinanceAPIException as e: - code = getattr(e, "code", None) - # -4061: positionSide 不匹配 -> 兜底切换一次 - if code == -4061: - retry = dict(params) - if "positionSide" in retry: - retry.pop("positionSide", None) - else: - retry["positionSide"] = "LONG" if pd == "BUY" else "SHORT" - return await self.client.futures_create_order(**retry) - raise + # 走 Algo Order 接口(避免 -4120) + order = await self.futures_create_algo_order(params) + if order: + return order + # 兜底:对冲/单向模式可能导致 positionSide 误判,尝试切换一次 + if dual is True: + retry = dict(params) + retry.pop("positionSide", None) + return await self.futures_create_algo_order(retry) + else: + retry = dict(params) + retry["positionSide"] = "LONG" if pd == "BUY" else "SHORT" + return await self.futures_create_algo_order(retry) except BinanceAPIException as e: # 常见:Order would immediately trigger(止损/止盈价不在正确一侧) diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 648d114..bfd0c42 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -656,7 +656,7 @@ class PositionManager: oid = info0.get(k) if oid: try: - await self.client.cancel_order(symbol, int(oid)) + await self.client.futures_cancel_algo_order(int(oid)) except Exception: pass info0.pop("exchangeSlOrderId", None) @@ -1090,7 +1090,7 @@ class PositionManager: # 防重复:先取消旧的保护单(仅取消特定类型,避免误伤普通挂单) try: - await self.client.cancel_open_orders_by_types( + await self.client.cancel_open_algo_orders_by_order_types( symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"} ) except Exception: @@ -1114,11 +1114,12 @@ class PositionManager: ) try: - position_info["exchangeSlOrderId"] = sl_order.get("orderId") if isinstance(sl_order, dict) else None + # Algo 接口返回 algoId + position_info["exchangeSlOrderId"] = sl_order.get("algoId") if isinstance(sl_order, dict) else None except Exception: position_info["exchangeSlOrderId"] = None try: - position_info["exchangeTpOrderId"] = tp_order.get("orderId") if isinstance(tp_order, dict) else None + position_info["exchangeTpOrderId"] = tp_order.get("algoId") if isinstance(tp_order, dict) else None except Exception: position_info["exchangeTpOrderId"] = None