This commit is contained in:
薇薇安 2026-01-19 22:59:02 +08:00
parent 5abd1c340c
commit f495815af6
3 changed files with 106 additions and 44 deletions

View File

@ -143,20 +143,11 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str):
except Exception: except Exception:
raise HTTPException(status_code=400, detail=f"{symbol} 数据库缺少止损/止盈价,且无法回退计算,无法补挂") raise HTTPException(status_code=400, detail=f"{symbol} 数据库缺少止损/止盈价,且无法回退计算,无法补挂")
# 5) 取消旧的保护单,避免重复 # 5) 取消旧的保护单Algo 条件单),避免重复
try: try:
orders = await client.client.futures_get_open_orders(symbol=symbol) await client.cancel_open_algo_orders_by_order_types(
for o in orders or []: symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}
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
except Exception: except Exception:
pass pass
@ -198,19 +189,22 @@ async def _ensure_exchange_sltp_for_symbol(symbol: str):
tp_round = "DOWN" if side == "BUY" else "UP" tp_round = "DOWN" if side == "BUY" else "UP"
close_side = "SELL" if side == "BUY" else "BUY" close_side = "SELL" if side == "BUY" else "BUY"
# Algo 条件单使用 triggerPrice不是 stopPrice
sl_params = { sl_params = {
"algoType": "CONDITIONAL",
"symbol": symbol, "symbol": symbol,
"side": close_side, "side": close_side,
"type": "STOP_MARKET", "type": "STOP_MARKET",
"stopPrice": _fmt(sl_price, sl_round), "triggerPrice": _fmt(sl_price, sl_round),
"closePosition": True, "closePosition": True,
"workingType": "MARK_PRICE", "workingType": "MARK_PRICE",
} }
tp_params = { tp_params = {
"algoType": "CONDITIONAL",
"symbol": symbol, "symbol": symbol,
"side": close_side, "side": close_side,
"type": "TAKE_PROFIT_MARKET", "type": "TAKE_PROFIT_MARKET",
"stopPrice": _fmt(tp_price, tp_round), "triggerPrice": _fmt(tp_price, tp_round),
"closePosition": True, "closePosition": True,
"workingType": "MARK_PRICE", "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" sl_params["positionSide"] = "LONG" if side == "BUY" else "SHORT"
tp_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) sl_order = await client.futures_create_algo_order(sl_params)
tp_order = await client.client.futures_create_order(**tp_params) tp_order = await client.futures_create_algo_order(tp_params)
# 再查一次未成交委托,确认是否真的挂上(并用于前端展示/排查) # 再查一次未成交委托,确认是否真的挂上(并用于前端展示/排查)
open_orders = [] open_orders = []
try: 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): if isinstance(oo, list):
for o in oo: for o in oo:
try: try:
if not isinstance(o, dict): if not isinstance(o, dict):
continue continue
otype2 = str(o.get("type") or "").upper() otype2 = str(o.get("orderType") or o.get("type") or "").upper()
if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}: if otype2 in {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET", "STOP", "TAKE_PROFIT"}:
open_orders.append( open_orders.append(
{ {
"orderId": o.get("orderId"), "algoId": o.get("algoId"),
"type": otype2, "orderType": otype2,
"side": o.get("side"), "side": o.get("side"),
"stopPrice": o.get("stopPrice"), "triggerPrice": o.get("triggerPrice"),
"price": o.get("price"),
"workingType": o.get("workingType"), "workingType": o.get("workingType"),
"positionSide": o.get("positionSide"), "positionSide": o.get("positionSide"),
"closePosition": o.get("closePosition"), "closePosition": o.get("closePosition"),
"status": o.get("status"), "algoStatus": o.get("algoStatus"),
"updateTime": o.get("updateTime"), "updateTime": o.get("updateTime"),
} }
) )

View File

@ -1298,6 +1298,45 @@ class BinanceClient:
logger.error(f"取消订单失败: {e}") logger.error(f"取消订单失败: {e}")
return False 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]: async def get_open_orders(self, symbol: str) -> List[Dict]:
""" """
获取某交易对的未成交委托用于防止重复挂保护单 获取某交易对的未成交委托用于防止重复挂保护单
@ -1336,6 +1375,33 @@ class BinanceClient:
except Exception: except Exception:
return 0 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( async def place_trigger_close_position_order(
self, self,
symbol: str, symbol: str,
@ -1422,32 +1488,34 @@ class BinanceClient:
stop_price_str = self._format_price_str_with_rounding(sp, symbol_info, rounding_mode) stop_price_str = self._format_price_str_with_rounding(sp, symbol_info, rounding_mode)
# Algo 条件单接口使用 triggerPrice不是 stopPrice
params: Dict[str, Any] = { params: Dict[str, Any] = {
"algoType": "CONDITIONAL",
"symbol": symbol, "symbol": symbol,
"side": close_side, "side": close_side,
"type": ttype, "type": ttype,
"stopPrice": stop_price_str, "triggerPrice": stop_price_str,
"closePosition": True,
"workingType": working_type, "workingType": working_type,
"closePosition": True,
} }
# 对冲模式:必须指定 positionSide # 对冲模式:必须指定 positionSide
if dual is True: if dual is True:
params["positionSide"] = "LONG" if pd == "BUY" else "SHORT" params["positionSide"] = "LONG" if pd == "BUY" else "SHORT"
try: # 走 Algo Order 接口(避免 -4120
return await self.client.futures_create_order(**params) order = await self.futures_create_algo_order(params)
except BinanceAPIException as e: if order:
code = getattr(e, "code", None) return order
# -4061: positionSide 不匹配 -> 兜底切换一次 # 兜底:对冲/单向模式可能导致 positionSide 误判,尝试切换一次
if code == -4061: if dual is True:
retry = dict(params) retry = dict(params)
if "positionSide" in retry:
retry.pop("positionSide", None) retry.pop("positionSide", None)
return await self.futures_create_algo_order(retry)
else: else:
retry = dict(params)
retry["positionSide"] = "LONG" if pd == "BUY" else "SHORT" retry["positionSide"] = "LONG" if pd == "BUY" else "SHORT"
return await self.client.futures_create_order(**retry) return await self.futures_create_algo_order(retry)
raise
except BinanceAPIException as e: except BinanceAPIException as e:
# 常见Order would immediately trigger止损/止盈价不在正确一侧) # 常见Order would immediately trigger止损/止盈价不在正确一侧)

View File

@ -656,7 +656,7 @@ class PositionManager:
oid = info0.get(k) oid = info0.get(k)
if oid: if oid:
try: try:
await self.client.cancel_order(symbol, int(oid)) await self.client.futures_cancel_algo_order(int(oid))
except Exception: except Exception:
pass pass
info0.pop("exchangeSlOrderId", None) info0.pop("exchangeSlOrderId", None)
@ -1090,7 +1090,7 @@ class PositionManager:
# 防重复:先取消旧的保护单(仅取消特定类型,避免误伤普通挂单) # 防重复:先取消旧的保护单(仅取消特定类型,避免误伤普通挂单)
try: 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"} symbol, {"STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET"}
) )
except Exception: except Exception:
@ -1114,11 +1114,12 @@ class PositionManager:
) )
try: 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: except Exception:
position_info["exchangeSlOrderId"] = None position_info["exchangeSlOrderId"] = None
try: 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: except Exception:
position_info["exchangeTpOrderId"] = None position_info["exchangeTpOrderId"] = None