a
This commit is contained in:
parent
5abd1c340c
commit
f495815af6
|
|
@ -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"),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
# 走 Algo Order 接口(避免 -4120)
|
||||
order = await self.futures_create_algo_order(params)
|
||||
if order:
|
||||
return order
|
||||
# 兜底:对冲/单向模式可能导致 positionSide 误判,尝试切换一次
|
||||
if dual is True:
|
||||
retry = dict(params)
|
||||
if "positionSide" in retry:
|
||||
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.client.futures_create_order(**retry)
|
||||
raise
|
||||
return await self.futures_create_algo_order(retry)
|
||||
|
||||
except BinanceAPIException as e:
|
||||
# 常见:Order would immediately trigger(止损/止盈价不在正确一侧)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user